-
Notifications
You must be signed in to change notification settings - Fork 0
/
interview_question.v0.0.1.txt
1515 lines (958 loc) · 52.6 KB
/
interview_question.v0.0.1.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1. Slice和数组
1.1. Go语言数组和切片有什么区别
a. 首先得先知道数组的底层原理:
1. 本质上是相同类型元素的集合组成的数据结构,
计算机会为数组分配一块连续的内存来保存其中的元素
2. 与很多语言不同,Go 语言中数组在初始化之后大小就无法改变。
3. 存储元素类型相同、但是大小不同的数组类型在 Go
语言看来也是完全不同的,只有两个条件都相同才是同一个类型
4. 获取数组大小、对数组中的元素的读写在编译期间就已经进行了简化
(
NOTE: 如果数组中元素的个数小于或者等于 4 个,
那么所有的变量会直接在栈上初始化,
如果数组元素大于 4个,变量就会在静态存储区初始化然后拷贝到栈上
(?这样做的原因是什么暂时不是很清楚?)
这个处理是在编译期间做的,这些转换后的代码才会
继续进入中间代码生成和机器码生成两个阶段,
最后生成可以执行的二进制文件。
NOTE:
1. Go 语言中对越界的判断是可以在编译期间由静态类型检查完成
数组和字符串的一些简单越界错误都会在编译期间发现,
比如我们直接使用整数或者常量访问数组
2. 但是如果使用变量去访问数组或者字符串时,
编译器就无法发现对应的错误了,
这时就需要 Go 语言运行时发挥作用了
)
b. 切片其实底层原理是围绕着数组进行的, 他的底层数据结构包括三部分。data len cap.
1. 首先data作为一个指针指向的数组是一片连续的内存空间.
在同一个底层数组被多个slice使用的时候,编程的时候就需要注意,
这个地方可能会有坑。
2. 现切片与数组的关系非常密切,切片引入了一个抽象层,
使用 len cap 两个属性提供了对数组中部分片段的引用
3. 如果底层的数组长度不足就会触发扩容机制,切片中的数组就会发
4. 不过在上层看来切片时是没有变化的,因为上层只需要与切片
打交道不需要关心底层的数组变化。
5. 切片是运行时才会确定内容的结构,所有的操作还需要依赖 Go 语言的运行时来完成
c. 就上面所以他们的不同常常体现在以下几点
1. 本质上的数据结构不一样,切片依赖数组,会保存一个底层数组的指针。
2. Go 语言中数组在初始化之后大小就无法改变,但是切片可以,他一套扩容机制
3. 切片有自身的扩容机制
4. 获取数组大小、对数组中的元素的读写在编译期间就已经进行了简化. 而切片的大部分操作
都需要在go运行时进行确定。
5. 并且因为底层数据结构不太一样的情况之下,他们所保存的数据的读取方式也不一样。
d. (还要描述一下切片的初始换、数组的初始化、数组的数据读取、切片中数据读取吗?)
1.2. Go语言切片的内存结构和扩容
a. 切片的数据结构如下
1. data -> 一个执行数组的指针, 本质上是一片连续的内存
2. len -> 当前切片的长度
3. cap -> 示当前切片的容量
b. 关于扩容,我们就要分成三部分进行讲解
1. 首先是什么条件之下才会触发扩容
1.1. 使用 append 关键字向切片追加元素。
新的slice长度大于容量就会扩容。
此项工作会有编译器会进行一定的处理。
(
编译器判断是否会覆盖原变量来
拆分 append 关键字,新建两个处理流程。
两种流程区别不大。
---『新切片』不需要赋值回原有的变量 ---
* append(slice, 1, 2, 3)
先对切片结构体进行解构获取它的数组指针、大小和容量
如果在追加元素后切片的大小大于容量
调用 runtime.growslice 对切片进行扩容并将新的元素依次加入切片
--- 如果 append 后的切片会覆盖原切片 ----
* slice = append(slice, 1, 2, 3)
此时会使用另一种方式改写关键字
)
2. 然后是扩容的具体操作
2.1. 扩容就是为切片分配一块新的内存空间并将原切片的元素全部拷贝过去
2.2. 在分配内存空间之前需要先确定新的切片容量
2.3. Go 语言根据切片的当前容量选择不同的策略进行扩容
2.3.1. 如果期望容量大于当前容量的两倍就会使用期望容量;
2.3.2. 如果当前切片容量小于 1024 就会将容量翻倍;
2.3.3. 如果当前切片容量大于 1024 就会每次增加 25% 的容量,
直到新容量大于期望容量;
2.4. 确定了切片的容量之后,就可以计算切片中新数组占用的内存
(
计算的方法就是将目标容量和元素大小相乘
计算新容量时可能会发生溢出或者请求的内存超过上限,在这时就会直接 panic
)
2.5. 最终会返回一个新的 slice 结构,其中包含了新的数组指针、大小和容量.
会改变原有的切片.
3. 就是扩容导致的后果,有哪些是需要我们注意的?
3.1. 由于扩容之后原切片的会被改变,底层的数组会被改变,
这个是我们编程的时候比较需要进行注意的点。
(
1. 多个slice共享一个底层数组的代码逻辑,他们之间是否有依赖?
2. 作为参数的时候,如果append之后,再去更改里面的值,不将新指针
返回,就不能达到预期。
)
1.3. slice 在for一遍之后会改变内容吗
(题意不明,如果是使用for range,如果用index去改变之,就是会改变的)
1.4. 数组是如何实现用下标访问任意元素的
1.4.1 数组是一片连续的内存,如果我们知道下标可以即可快速地算出每一个元素的内存地址
addr = sizeof(Type) * idx + addr_head
1.4.2 当我们知道虚拟内存的地址的时候,系统就可以通过 MMU 直接翻译成物理地址,读取到
物理内存中的内容
1.5. 如何复制切片
a. 如果需要的是深拷贝
1. 使用copy()函数进行拷贝,但是需要注意的是
[目标切片必须分配过空间且足够承载复制的元素个数]
[当我们使用copy(a, b) 的形式对切片进行拷贝时,
编译期间会分两种情况进行处理]
[相比于依次对元素进行拷贝,这种方式能够提供更好的性能,
最终都会使用memmove复制内存快]
[哪怕使用 memmove 对内存成块进行拷贝,
但是这个操作还是会占用非常多的资源,
在大切片上执行拷贝操作时一定要注意性能影响]
2. 使用for range,遍历切片元素, append进新数组。
3. 或直接append整个数组进去
new := append(new, old...)
b. 如果需要的是浅拷贝,直接赋值就可以,但是
需要注意由于slice的底层原理,新旧slice都用的
同一个数组的指针,会出现因共享内存而引入的风险。
new := old
2. Channel
1. 介绍一下通道
a. 背景
a1. 永远不要通过共享内存来通信,而是要通过通信来共享内存,
channel便是用于实现goroutine间通信的
a2. channel提供了三种类型
1. 单向只能发送
2. 单向只能接收
3. 双向即可发送也可接收
b. 那就先介绍一下通道的底层原理 (简单说一说本质就可以了)
b1. buf是带缓冲的channle所特有的结构,
是个循环链表,用来存储缓存数据
b2. sendx和recvx是用于记录buf中发送和接收的index
b3. lock是个互斥锁,目的是为了保证
goroutine以先进先出FIFO的方式进入结构体
b4. recvq和sendq分别是往channel接收或
发送数据的goroutine所抽象出来的数
据结构,是个双向链表
c. 再介绍一下通道的优点
c1. 通过提供原子的通信原语,
避免了竞态情形(race condition)下复杂的锁机制。
d. 再介绍一下通道常常使用在什么地方
d1. 消息传递、消息过滤
d2. 信号广播
d3. 事件订阅与广播
d4. 请求、响应转发
d5. 任务分发
d6. 结果汇总
d7. 并发控制
d8. 同步与异步
e. 最后,通道有一些特性是我们在进行开发的时候常常需要注意的:(未完,简单例子)
1. 给一个 nil channel 发送数据,造成永远阻塞
2. 从一个 nil channel 接收数据,造成永远阻塞
3. 给一个已经关闭的 channel 发送数据,引起 panic
4. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
5. 无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的
6. 关闭一个 nil channel 将会发生 panic
7. 有缓存channel,取出数据的顺序是乱序的
8. channel缓存的大小不是并发度
2. 同一个协程里面,对无缓冲channel同时发送和接收数据有什么问题
当前协程会永远阻塞
3. channel和锁对比一下
1. channel 的具体实现简单来说是使用一个 ring buffer 、互斥锁、发送队列、接收队列
来实现的。也就是说,channel 的实现本来就是依赖锁的。本质上是通过锁和“值的拷贝”
实现的。
** https://www.bookstack.cn/read/qcrao-Go-Questions/channel-channel%20%E5%8F%91%E9%80%81%E5%92%8C%E6%8E%A5%E6%94%B6%E5%85%83%E7%B4%A0%E7%9A%84%E6%9C%AC%E8%B4%A8%E6%98%AF%E4%BB%80%E4%B9%88.md **
** 本质上不是共享内存 **
2. 使用channel的话,我们就可以使用官方给封装好的一套经过他们验证的机制.
并且使用channel的CSP模型进行并发模型构建也能让人更加容易理解。
这能加快开发效率,提高安全性。
3. 使用Mutex的话,如果整个锁被多段代码使用的话,慢慢地就比较难去理解完整的
逻辑了。将锁分布在整个代码中,是一场噩梦,会让我们慢慢失去对代码的掌控。
组合使用channel是比组合使用锁是更容易理解的。
4. 但是有些时候,我们也需要使用锁。如果我们试图保存某个数据结构的状态,
这个适合使用锁会比使用channel方便,简单。
```
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Incerment {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
```
5. 有时候,在软件的瓶颈初,因为锁的实现没有channel的底层机制复杂,使用
channel可能会有较差的性能,我们可以使用锁(但是出现这种情况,我们往往
需要重构软件,才能获取更高性能)。
4. channel的应用场景
4.1. 消息传递、消息过滤
4.2. 信号广播
4.3. 事件订阅与广播
4.4. 请求、响应转发
4.5. 任务分发
4.6. 结果汇总
4.7. 并发控制
4.8. 同步与异步
5. channel和共享内存有什么优劣势?
与第三题的描述一致
6. go channel使用协程交替打印数字和字符
第一题:
https://www.kdocs.cn/l/ssUGatGrktgs?f=201
8. Go 语言当中 Channel 缓冲有什么特点?
a.无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的
b. 有缓存channel,取出数据的顺序是乱序的
c. channel缓存的大小不是并发度
9. Go 语言中 cap 函数可以作用于那些内容?
array(数组)
slice(切片)
channel(通道)
10. Go 语言的 Channel 特性?(Go 语言当中 Channel(通道)有什么特点,需要注意什么?)
1. 给一个 nil channel 发送数据,造成永远阻塞
2. 从一个 nil channel 接收数据,造成永远阻塞
3. 给一个已经关闭的 channel 发送数据,引起 panic
4. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
5. 无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的
6. 关闭一个 nil channel 将会发生 panic
7. 有缓存channel,取出数据的顺序是乱序的
8. channel缓存的大小不是并发度
11. Channel 的 ring buffer 实现
1. channel 中使用了 ring buffer(环形缓冲区) 来缓存写入的数据。ring
buffer 有很多好处,而且非常适合用来实现 FIFO 式的固定长度队列。
2.在 channel 中,ring buffer 的实现如下:
1. hchan 中有两个与 buffer 相关的变量:recvx 和 sendx。
其中 sendx 表示buffer 中可写的 index,
recvx 表示 buffer 中可读的 index。
2. 从 recvx 到 sendx 之间的元素,表示已正常存放入 buffer 中的数据。
我们可以直接使用 buf[recvx]来读取到队列的第一个元素,
使用 buf[sendx] = x 来将元素放到队尾。
12. for循环select时,如果通道已经关闭会怎么样?如果select中的case只有一个,又会怎么样?
a. 尝试读取一个已经关闭的通道,如果是缓冲的Channel,就会优先
返回缓冲区里面的数据,直到缓冲区没有数据后一直返回零值。
如果是没缓冲区的channel,就会一直返回零值。
b. 如果只有一个case,就会一直执行这个case里面的代码逻辑。
c. 如果有多个case,那就视具体情况,随机执行。
13. 读或写一个已经关闭的channel会发生什么问题?
1.有缓存,
读:会把缓存中的数据读出,第二个返回值是true;
数据读完返回零值,第二个返回值是false.
写:会报错
2.无缓存
读:返回零值,第二个返回值是false
写:会报错 panic
3. 锁
1. go用共享内存的方式实现并发如何保证安全?
** 总结自:<<go语言设计与实现>> **
1.1. 通常来说,使用共享内存的方式进行协程之间的沟通的话,
是要使用锁来保证安全的。
1.2. go语言提供了比较多的同步原语,防止多个goroutine访问同一个
内存地址时出现混乱的情况, 最常用的就是以下几个:
1.2.1 Mutex
1.2.2 RWMutex
1.2.3 WaitGroup
1.2.4 Once
1.2.5 Cond
1.3. Mutex和RWMutex 是通过锁定临界区中的数据,让goroutine
串行执行来保证共享内存的安全的。
1.3. 而WaitGroup、Once、Cond 主要是通过实现同步机制来
来实现共享内存的并发和安全的。(描述得可能不是十分好)
2. go的锁是可重入的吗?
总结自:<<go语言设计与实现>>
2.1. go中的锁,不是可重入的
2.2. 具体原因与Mutex的实现原理有关
2.2.1. 如果发现锁已经被锁,就会尝试进入自旋,自旋后就会陷入休眠
2.2.2. 假如我们在同一个goroutine中Lock之后再尝试Lock, 就会导致
程序的第二个Lock之后,导致程序走向休眠,也算是一种死锁
(线程一致在休眠,等待自身锁定的锁的释放,但是因为自身已经休眠
释放锁的逻辑是永远没有办法被执行的,所以是死锁)
2.2.3. 这会导致Lock之后的流程无法被执行,一般来说都是一个错误的实现
3. 获取不到锁会一直等待吗?
** 总结自:<<go语言设计与实现>> **
3.1. 获取不到锁goroutine一直等待锁的获取,那是肯定的
3.2. 但是获取不到锁的goroutine,却不会永远地占有CPU
3.3. 首先当前goroutine尝试进入自旋操作,自旋时会一直在CPU上运行,
并期望在自旋期间能获取到锁
3.4. 如果在自旋期间没有能获取到锁,就会将自身休眠,让出CPU,等待
下一次唤醒。
3.5. 并且如果锁一直在等待,超过了1ms,将锁的模式更改为饥饿模式
来增大自身被调度到的机会。
4. 如何实现一个timeout的锁?
( https://stackoverflow.com/questions/54488284/attempting-to-acquire-a-lock-with-a-deadline-in-golang )
( 模仿: https://segmentfault.com/a/1190000015805060 )
1.实现一
其他百度出来的方法都是非常难的,用的test-and-set的方式进行,
并且没经过验证,十分有可能也是错的
```
select {
case l <- struct{}{}:
// lock acquired
<-l
case <-time.After(time.Minute):
// lock not acquired
}
```
5. Go 当中同步锁有什么特点?作用是什么
** 来源:个人理解 **
5.1 特点
5.1.1 不可重入
5.1.2 会尝试自旋
5.1.3 自旋失败会尝试休眠, 让出CPU给其他goroutine
5.1.4 超过1ms未被调度,将会进入饥饿模式
5.2 作用
5.2.1 保证一段共享的内存不会在同时被其他goroutine
更改, 保证数据的原子性
5.3.1 提供多一种并发操作的手段,在channel之外有其他
选择,提供了灵活性
6. Mutex 几种状态
信息来源:<<2020工程师大厂面试题>>
6.1. mutexLocked — 表示互斥锁的锁定状态;
6.2. mutexWoken — 表示从正常模式被从唤醒;
6.3. mutexStarving — 当前的互斥锁进入饥饿状态;
6.4. waitersCount — 当前互斥锁上等待的 Goroutine 个数;
7. Mutex 正常模式和饥饿模式
** 大部分信息来源:<<2020工程师大厂面试题>> **
7.1. 首先说一下什么是正常模式, 什么是饥饿模式
7.1.1 正常模式下,所有等待锁的 goroutine 按照 FIFO(先进先出)顺序等待。唤醒的
goroutine 不会直接拥有锁,而是会和新请求锁的 goroutine 竞争锁的拥有。
7.1.2 饥饿模式下,直接把锁交给等待队列中排在第一位的 G(队头),同
时,饥饿模式下,新进来的 G 不会参与抢锁也不会进入自旋状态,会直接进入
等待队列的尾部
7.2. 之所以分出两种模式,是因为
7.2.1 新请求锁的 goroutine 具有优势:它正在 CPU 上执行,而且可能有好几个。
所以刚刚唤醒的 goroutine 有很大可能在锁竞争中失败
7.2.2 在这种情况下,这个被唤醒的 goroutine 会加入到等待队列的前面,再次等待
被唤醒
7.3. 正常模式和饥饿模式之间切换条件:
7.3.1 如果一个等待的 goroutine 超过 1ms 没有获取锁,那么它将会把锁转变为饥饿模式
7.3.2 当前队列只剩下一个 g 的时候,Mutex 切换到饥饿模式
7.3.2 当前互斥锁处于饥饿模式时,如果该 Goroutine 是队列中最后的一个 Goroutine
或者等待锁的时间小于 starvationThresholdNs(1ms),
当前 Goroutine 就会直接获得互斥锁并且从饥饿模式中退出
(这段是<<Go语言设计与实现>>书中 220 页的描述)
7.4. 优缺点
7.4.1 对于两种模式,正常模式下的性能是最好的,goroutine 可以连续多次获取
7.4.2 饥饿模式解决了取锁公平的问题,但是性能会下降
7.4.3 其实是性能和公平的一个平衡模式
8. Mutex 允许自旋的条件
** 大部分信息来源:<<2020工程师大厂面试题>> **
8.1 锁已被占用,并且锁不处于饥饿模式。
8.2 积累的自旋次数小于最大自旋次数(active_spin=4)。
8.3 cpu 核数大于 1。
8.4 有空闲的 P。
8.5 当前 goroutine 所挂载的 P 下,本地待运行队列为空。
9. RWMutex 实现
** 本问题主要通过看 <<go语言设计与实现>> 进行的总结 **
```
type RWMutex struct {
w Mutex
writerSem uint32
readerSem uint32
readerCount int32 // 存储了当前正在执行的读操作的数量
readerWait int32 // 表示当写操作被阻塞时等待的读操作个数
}
```
9.1. RWMutex 实际上是通过对Mutex的进一步的封装进行的
9.2. 主要是为了满足常见的服务对资源的读写比例会非常高,
如果大多数的请求都是读请求,它们之间不应该互相影响的
9.3. RWMutex 读写互斥锁解决的问题:
不限制对资源的并发读,但是读写、写写操作无法并行执行
9.4. 通常不一样的就是,他分别有两种锁,一种是读锁,一种是读写锁
9.4.1. 在为资源加上读锁然后解锁的时候,流程是这样的:
a. 尝试为readerCount 加 1,如果返回负数就说明有goroutine上了读写锁
正在占用资源,然后陷入休眠。如果成功就直接返回
b. 解锁的时候,尝试减少 readerCount ,如果返回负数就说明此时有一个正在进行的
写操作(这个写操作,是需要等到所有拥有读锁的干完了活,才可以重新调度的)。
此时拥有读锁的goroutine,就要去减少readerWait
c. 在期间的读操作都释放之后,触发 writerSem ,唤醒当前拥有读写锁的线程
9.4.2 在为资源加上读写锁的时候,流程如下:
a. 首先调用了读写互斥锁持有的 Mutex 的 Lock 方法保证
其他获取读写锁的 Goroutine 进入等待状态
b. 随后 阻塞后续的读操作
(
通过将readercount取负数,让后续reader进入锁的时候
无法完成 readercount + 1 > 0 ,从而进入休眠
)
c. 如果当前仍然有其他 Goroutine 持有互斥锁的读锁,
该 Goroutine 就会调用runtime_SemacquireMutex 进入休眠状态,
等待读锁释放时触发 writerSem 信号量将当前协程唤醒。
d. 解锁的时候
d1. 对资源的读写操作完成之后就会将 readerCount 变为正数
并触发所有由于获取读锁而陷入等待的 Goroutine
d2. 最后释放持有的互斥锁让其他的协程能够重新获取读写锁。
10. RWMutex 注意事项
** 信息来源:<<2020工程师大厂面试题>> **
10.1. RWMutex 类型变量的零值是一个未锁定状态的互斥锁。
10.2. RWMutex 在首次被使用之后就不能再被拷贝。
10.3. RWMutex 的读锁或写锁在未锁定状态,解锁操作都会引发 panic。
10.4. RWMutex 的一个写锁 Lock 去锁定临界区的共享资源,如果临界区的共享
资源已被(读锁或写锁)锁定,这个写锁操作的 goroutine 将被阻塞直到
解锁。
10.5. RWMutex 的读锁不要用于递归调用,比较容易产生死锁。
10.6. RWMutex 的锁定状态与特定的 goroutine 没有关联。一个 goroutine 可
以 RLock(Lock),另一个 goroutine 可以 RUnlock(Unlock)。
10.7. 写锁被解锁后,所有因操作锁定读锁而被阻塞的 goroutine 会被唤醒,并
都可以成功锁定读锁。
10.8. 读锁被解锁后,在没有被其他读锁锁定的前提下,所有因操作锁定写锁而
被阻塞的 goroutine,其中等待时间最长的一个 goroutine 会被唤醒。
11. Cond 是什么
** 信息来源:<<2020工程师大厂面试题>> **
11.1. Cond 实现了一种条件变量,可以使用在多个 Reader 等待共享资源 ready 的场景
11.2. 每个 Cond 都会关联一个 Lock ( Mutex 或 RWMutex 都可 ) , 当修改条件或者调用 Wait 方法时,
必须加锁,保护 condition
11.3. 一些用法例子如下:
12. Broadcast 和 Signal 区别
** 信息来源:<<2020工程师大厂面试题>> **
12.1 Broadcast 会唤醒所有等待 Cond 的 goroutine。
调用 Broadcast 的时候,可以加锁,也可以不加锁。
12.1 Signal 只唤醒 1 个等待 Cond 的 goroutine
调用 Signal 的时候,可以加锁,也可以不加锁。
13. Cond 中 Wait 使用
** 信息来源:<<2020工程师大厂面试题>> **
14. WaitGroup 用法
** 信息来源:<<2020工程师大厂面试题>> **
14.1 一个 WaitGroup 对象可以等待一组协程结束
14.2 使用方法如下:
14.2.1. main 协程通过调用 wg.Add(delta int) 设置 worker 协程的个数,然后创
建 worker 协程
14.2.2 worker 协程执行结束以后,都要调用 wg.Done();
14.2.3 main 协程调用 wg.Wait() 且被 block,直到所有 worker 协程全部执行结束
后返回
15. WaitGroup 实现原理
** 来自于 <<go语言设计与实现>> **
** 结构体 **
```
type WaitGroup struct {
noCopy noCopy // 主要作用就是保证 WaitGroup 不会被开发者通过再赋值的方式进行拷贝,进而导致一些诡异的行为
state1 [3]uint32 // 32位机器上 sema waiter counter
}
```
15.1 作用和常见使用场景如下
15.1.1 它可以用于等待一系列的 Goroutine 的返回
15.1.2 通过 WaitGroup 我们可以在多个 Goroutine 之间非常轻松地同步信息,
原本顺序执行的代码也可以在多个Goroutine 中并发执行,加快了程序处理的速度
15.1.3 比较常见的使用场景是批量执行 RPC 或者调用外部服务
15.2 WaitGroup 向外暴露了三个接口 Add, Done, Wait, 其中Done实际上是调用了 Add(-1).
我们需要了解的是Add、Wait 即可
15.2.1 介绍一下Add
a. 主要作用就是更新 WaitGroup 中持有的计数器 counter
b. 方法传入的参数可以为负数,但是一个 WaitGroup 的计数器只能是非负数
c. 当调用 Add 方法导致计数器归零并且还有等待的 Goroutine 时,
就会通过 runtime_Semrelease 唤醒处于等待状态的所有 Goroutine。
15.2.2 介绍一下 Wait
a. 在当前计数器中保存的数据大于 0 时修改等待 Goroutine 的个数
b. 调用 runtime_Semacquire 陷入睡眠状态
c. 陷入睡眠的 Goroutine 就会等待 Add 方法在计数器为 0 时唤醒
15.3 需要注意的点
15.3.1 Add 不能在和 Wait 方法在 Goroutine 中并发调用,一旦出现就会造成程序崩溃
15.3.2 WaitGroup 必须在 Wait 方法返回之后才能被重新使用;
15.3.3 可以同时有多个 Goroutine 等待当前 WaitGroup 计数器的归零,
这些 Goroutine 也会被『同时』唤醒
16. 什么是 sync.Once
** << 工程师大厂面试题 >> **
16.1. Once 可以用来执行且仅仅执行一次动作,常常用于单例对象的初始化场景。
16.2. Once 常常用来初始化单例资源,或者并发访问只需初始化一次的共享资
源,或者在测试的时候初始化一次测试资源。
16.3. sync.Once 只暴露了一个方法 Do,你可以多次调用 Do 方法,但是只有第
一次调用 Do 方法时 f 参数才会执行,这里的 f 是一个无参数无返回值
的函数
17. 什么操作叫做原子操作
** << 工程师大厂面试题 >> **
1. 一个或者多个操作在 CPU 执行过程中不被中断的特性,称为原子性
2. 这些操作对外表现成一个不可分割的整体,他们要么都执行,要
么都不执行,外界不会看到他们只执行到一半的状态。
3. 而在现实世界中,CPU不可能不中断的执行一系列操作,
因为随时会有软中断、硬中断、时钟终端、系统调用等trap
影响着计算机的调度器,不同线程随时都可能切换执行,
执行顺序无法被保证。
4. 但如果我们在执行多个操作时,能让他们的中间状态对外不可见,
那我们就可以宣称他们拥有了“不可分割”的原子性。
5. 在 Go 中,一条普通的赋值语句其实不是一个原子操作。会被编译成多条CPU
指令来执行
5. 如果需要原子的语句,我们需要用CAS技术,并且在这之上,实现我们
对外抽象的 “ 不可分割” 的原子性。
18. 原子操作和锁的区别
1. 原子操作由底层硬件支持,而锁则由操作系统的调度器实现
2. 锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常会更有效率
并且更能利用计算机多核的优势
3. 如果要更新的是一个复合对象,
则应当使用atomic.Value 封装好的实现
19. 什么是 CAS
** << 工程师大厂面试题 >> **
** 可以看 << 操作系统导论 >> 来增强个人理解 **
1. CAS 的全称为 Compare And Swap,直译就是比较交换
2. 其作用是让 CPU 先进行比较两个值是否相等,然后原子地更新某个位置的值,
其实现方式是给予硬件平台的汇编指令,在 intel 的 CPU 中,
使用的cmpxchg 指令,就是说 CAS 是靠硬件实现的,从而在硬件层面提升效率
4. Interface
4.1. go里面interface是什么概念
1. 接口的本质:
a. 是引入一个新的中间层,调用方可以通过调用接口,
而不用去关注接口内部的具体实现,实现上下游的解耦。
b. 上下层的解耦是非常好的,这样非常有利于代码的分层管理
分层代码的版本迭代、有很强的可移植性
2. GO接口的特征
a. 接口的实现都是隐式的
b. 有两种方式进行接口的实现
b1. 接口体实现接口
b2. 接口体指针实现接口
c. 并且有两种方式初始化变量
c1. 结构体初始化变量
c2. 结构体指针初始化变量
d. 使用结构体实现接口带来的开销
会大于使用指针实现,而动态派发
在结构体上的表现非常差,
我们应当尽量避免使用结构体类型实现接口。
(
动态派发:运行期间选择具体多态操作(方法或者函数)执行的过程
)
3. 使用方式
3.1 一个类型可以实现多个接口而接口间彼此独立,不知道对方的实现。
3.2 一个接口可以有多个结构实现
4.2. Go 两个接口之间可以存在什么关系?
1. 两个接口可以同时被一个结构体实现,但是接口之间互相独立
2. 也可以被不同的结构体分别实现
5. String 和 Byte
5.1. string和byte数组有什么区别?
5.1.1 string类型,其实他底层本质就是一个byte类型的数组
5.1.2 string类型被设计为不可变的,
在并发场景下,我们可以在不加锁的控制下,
多次使用同一字符串,在保证高效共享的情况下而不用担心安全问题。
5.1.3 每一个更改字符串,就需要重新分配一次内存
5.1.4 实际上,string 的结构体相比于 byte 数组,
只有一个不同, 就是 cap。他们在源码中的转换是
非常快速的。例如string 转为 byte 数组,只需要
把 算出自己的 cap 转成 byte 即可。
** https://medium.com/@kevinbai/golang-%E4%B8%AD-string-%E4%B8%8E-byte-%E4%BA%92%E8%BD%AC%E4%BC%98%E5%8C%96-6651feb4e1f2 **
6. struct
6.1. struct之间能比较吗,什么情况下能比较?
分为两种情况 (cnblogs.com/dashu-saycode/p/14286228.html)
a. 同一个struct的不同示例比较
Complex(复数型),Pointer,Channel,Interface,Array
的可以比较。否则不可比较
2. 如果机构体里的元素类型有
Slice,Map,Function
则不可比较,在尝试编译的时候,就会报错
b. 不同struct的实例进行比较
1. 如果两个struct都只包含可比较的元素类型的时候,
并且struct中的元素数据类型一致,通过强制转换可以比较
(顺序不一样不可以比较,不能强制转换,无法通过编译)
2. 否则不可比较
c. 可以使用reflect.DeepEqual函数进行比较
(
具体对比规则需要进行补充操作
)
6.2. go结构体和结构体指针的区别
** 来源:https://segmentfault.com/a/1190000040129295 **
** TODO 答案十分一般,需要重新补充 **
6.2.1 在使用上的考虑:方法是否需要修改接收器?
如果需要,接收器必须是一个指针。
6.2.2 在效率上的考虑:如果接收器很大,
比如:一个大的结构体,使用指针接收器会好很多。
6.2.3 在一致性上的考虑:如果类型的某些方法必须有指针接收器,
那么其余的方法也应该有指针接收器,
所以无论类型如何使用,方法集都是一致的。
6.3. 空struct应用场景
a. golang 正常的 struct 就是普通的一个内存块,必定是占用一小块内存的.
并且结构体的大小是要经过边界,长度的对齐的,
但是“空结构体”是不占内存的,size 为 0.
b. 本质上来讲,使用空结构体的初衷只有一个:节省内存,但是更多的情况,
节省的内存其实很有限,这种情况使用空结构体的考量其实是:
根本不关心结构体变量的值。
c. 主要分为三块
1. 实现方法接收者
有时候我们需要将一些 <方法> 分类起来,便于后续拓展和维护。
如果我们使用:
比如有时候我们需要实现一个可以进行加法计算的函数,
和一个减法计算的函数。此时我们认为他是有类似的,需要
进行计算操作的函数。并可能后续会扩展其他的,如
指数运算的功能,并将这些方法扩展成一个计算器(calculator)。
这时,我们需要一个 <数据类型> 来管理他,但是不希望增加
计算机的负载。在这种情况下,如果我们用int、string等type
会消耗一定的内存,产生很多小内存对象,但是使用空struct
就不会出现这种情况,因为空struct不会消耗内存。
--- 分组1
type calculateInt struct{}
func (c calculateInt) add(x,y int) int {
return x+y
}
func (c calculateInt) sub(x,y int) int {
return x-y
}
--- 分组1
--- 分组2
type calculateInt2 struct{}
func (c calculateInt2) add(x,y int) int {
return x+y
}
func (c calculateInt2) sub(x,y int) int {
return x-y
}
--- 分组2
2. 实现集合类型
a. 在 Go 语言的标准库中并没有提供集合(Set)的相关实现,
因此一般在代码中我们图方便 (仅仅是图方便,不一定性能好,
因为map底层机制,会扩容,用空间换时间) ,
会直接用 map 来替代。
b. 但有个问题,就是集合类型的使用,
只需要用到 key(键),不需要 value(值)。
c. 空结构体作为占位符,不会额外增加不必要的内存开销,
很方便的就是解决了。
--- 例1 ---
type Set map[string]struct{}
func (s Set) Append(k string) {
s[k] = struct{}{}
}
func (s Set) Remove(k string) {
delete(s, k)
}
func (s Set) Exist(k string) bool {
_, ok := s[k]
return ok
}
func main() {
set := Set{}
set.Append("煎鱼")
set.Append("咸鱼")
set.Append("蒸鱼")
set.Remove("煎鱼")
fmt.Println(set.Exist("煎鱼"))
}
--- 例1 ---
3. 实现空通道
a. 在 Go channel 的使用场景中,常常会遇到通知型 channel,
其不需要发送任何数据,只是用于协调 Goroutine 的运行,
用于流转各类状态或是控制并发情况。
7. MAP
7.1. hash冲突解决办法,有什么弊端
7.2. map里面解决hash冲突怎么做的,冲突了元素放在头还是尾
7.3. map取一个key,然后修改这个值,原map数据的值会不会变化
7.4. map如何顺序读取
7.5. Golang Map 底层实现
7.6. Golang Map 如何扩容
7.7. Golang Map 查找
7.8. 如何复制map
a. 其实是不能拷贝的,如果想要拷贝一个 map ,只有一种办法就是循环赋值
b. 如果map中元素保存了指针,则需进一步考虑指针元素的深拷贝问题
(
问了再答?:
map作为一个封装好的数据结构,由于它底层可能会由于数据扩张而进行迁移,
所以拒绝直接寻址,避免产生野指针。
map中的key在不存在的时候,赋值语句其实会进行新的k-v值的插入,
所以拒绝直接寻址结构体内的字段,以防结构体不存在的时候可能造成的错误
)
c. 最简单的方法是用json unmarshal,变成字符串,
然后再用 json marshal生成新的map。
这种方法对结构体也适用,但是性能较差。