前两天看到了这个帖子,https://www.v2ex.com/t/547045#reply53 感觉其中描述的问题很有意思,我就用 Go 把它提到的解决方案都实现了一遍,文章链接:一条面试题引发的关于 Go 语言中锁的思考,还请大家多多指教!
前两天在 V2EX 上看到了一篇博文,《一条经典面试题引发的思考》,感觉很有意思,我就用 Go 把它的答案实现了一遍。
问题如下:
编写一个程序,开启 3 个线程 A,B,C,这三个线程的输出分别为 A、B、C,每个线程将自己的 输出在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如:ABCABCABC....
一个错误解法如下:
1 package main
2
3 import (
4 "fmt"
5 "sync"
6 )
7
8 var mu sync.Mutex
9 var index int
10 var endSignal = make(chan struct{})
11
12 func echoRoutine(routineIndex int, routineName string) {
13 for index < 30 {
14 mu.Lock()
15 if index%3 == routineIndex {
16 fmt.Println(index, routineName)
17 index++
18 }
19 mu.Unlock()
20 }
21
22 endSignal <- struct{}{}
23 }
24
25 func main() {
26 routineNames := []string{"A", "B", "C"}
27
28 for idx, name := range routineNames {
29 go echoRoutine(idx, name)
30 }
31
32 for _ = range routineNames {
33 <-endSignal
34 }
35 //Output:
36 //0 A
37 //1 B
38 //2 C
39 //3 A
40 //4 B
41 //5 C
42 //6 A
43 //7 B
44 //8 C
45 //9 A
46 //10 B
47 //11 C
48 //12 A
49 //13 B
50 //14 C
51 //15 A
52 //16 B
53 //17 C
54 //18 A
55 //19 B
56 //20 C
57 //21 A
58 //22 B
59 //23 C
60 //24 A
61 //25 B
62 //26 C
63 //27 A
64 //28 B
65 //29 C
66 //30 A
67 //31 B
68 }
从上面的输出可以看到,程序还额外输出了两次 A 和 B。首先说结论,这是因为检查条件index < 30
没有加锁。为了理解这个错误,首先我们需要了解一下 Go 的内存模型。
首先说什么是 Go 的内存模型,在官方文档中是这样定义的:
The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.
Go 语言的内存模型规定了一个 Goroutine 可以看见另外一个 Goroutine 修改同一个变量的值的条件,这类似于 Java 内存模型中 内存可见性 问题。
在 Go 语言中,编译器和处理器会对执行的指令进行重排序。Go 语言会保证的是,在单个 Goroutine 内,重排序后的执行效果和未进行重排序(即在代码中定义的执行顺序)的执行效果是一样。但是在多个 Goroutine 的运行环境下,由于重排序的原因,一个 Goroutine 观察到的执行顺序可能和另外一个 Goroutine 观察到的不一样。例如一个 Goroutine 执行a = 1; b = 2
,另一个 Goroutine 可能会在a
被赋值之前先观察到b
被赋值了。
为了规范读和写的操作,Go 定义了 happens before 原则。对于变量读写操作,我们可以将其称作是事件。事件之间的顺序可以定义为三种,E1 发生在 E2 之前,E1 发生在 E2 之后,E1 和 E2 同时发生。
在单个 Goroutine 内,happens before 顺序就是程序执行的顺序,如果下面两个条件都被满足的话,变量v
的读取事件r
,可以观察到变量v
的写入事件w
。
r
没有在w
之前发生。w
之后,r
之前,没有另外一个变量v
的写入事件w'
发生。总的来说,在单个 Goroutine 的环境下,读事件r
会观察到最近的一个写事件w
。
在多个 Goroutine 的运行环境下,如果需要让v
的读取事件r
观察到其写入事件w
。需要满足更严格的条件:
w
发生在r
之前v
的写入事件都发生在w
之前或者r
之后。因为在多个 Goroutine 的运行环境下,读写事件可能并行地发生,所以第一点更严格了一些,要求w
必须在r
之前发生,两者不能同时发生,且它们之间没有其他的写事件w'
。
因此在多个 Goroutine 访问一个共享变量v
的时候,我们必须使用同步事件去建立一个 happens before 条件来让读事件观察到目标写事件。
此外,还需要注意的是:
v
发生写事件之前,它被初始化成对应类型的零值。了解了 Go 内存模型中的 Happens Before 原则之后,我们接着再来分析上述的错误代码。
i++
的写操作和 A 和 B 执行第 13 行index < 30
读操作是并行发生的,所以 A 和 B 可能读取到的值是 29 并进入循环。index
的值,所以最后多输出的结果是 30 和 31。理解了这个错误之后,我们可以把代码稍微改一下,将index < 30
这个比较操作也置于锁的保护中,就能够得到正确的结果了。
package main
import (
"fmt"
"sync"
)
var i int
var mu sync.Mutex
var endSignal = make(chan struct{})
func threadPrint(threadNum int, threadName string) {
for {
mu.Lock()
if i >= 30 {
mu.Unlock()
break
}
if i%3 == threadNum {
fmt.Printf("%d: %s\n", i, threadName)
i++
}
mu.Unlock()
}
endSignal <- struct{}{}
}
func main() {
names := []string{"A", "B", "C"}
for idx, name := range names {
go threadPrint(idx, name)
}
for _ = range names {
<-endSignal
}
}
上述代码是得到的结果是正确的,但是还存在一些问题。我们想要的执行顺讯是有序的,
但每次解锁的时候,都是 A, B, C 三个 Goroutine 上去抢锁资源。有时候某个 Goroutine 抢到了锁资源,但是其并不符合执行要求(i%3 != threadNum
)。它就会将锁释放,然后让大家重新再来抢。
这样的争抢索锁资源造成了时间上的浪费,执行了一些不必要的操作。
为了避免这些浪费,我们可以参考 Java 中的公平锁,让解锁的顺序和上锁的顺序匹配,每次只有一个 Goroutine 获得锁资源,大家不必每次都去争抢锁资源。
package main
import (
"fmt"
"log"
"sync"
)
type FailLock struct {
mu *sync.Mutex
cond *sync.Cond
holdCount int
}
func NewFailLock() sync.Locker {
mu := new(sync.Mutex)
cond := sync.NewCond(mu)
return &FailLock{
holdCount: 0,
mu: mu,
cond: cond,
}
}
func (fl *FailLock) Lock() {
fl.mu.Lock()
defer fl.mu.Unlock()
fl.holdCount++
if fl.holdCount == 1 {
return
}
fl.cond.Wait()
}
func (fl *FailLock) Unlock() {
fl.mu.Lock()
defer fl.mu.Unlock()
if fl.holdCount == 0 {
log.Fatal("unlock of UnLocked mutex")
}
fl.holdCount--
if fl.holdCount != 0 {
fl.cond.Signal()
}
}
var (
end = make(chan struct{})
i int
)
func threadPrint(threadNum int, threadName string, mu sync.Locker) {
for i < 30 {
mu.Lock()
if i >= 30 {
mu.Unlock()
continue
}
if i < 3 && i%3 != threadNum {
mu.Unlock()
continue
}
fmt.Printf("%d: %s\n", i, threadName)
i += 1
mu.Unlock()
}
end <- struct{}{}
}
func main() {
mu := NewFailLock()
names := []string{"A", "B", "C"}
for idx, name := range names {
go threadPrint(idx, name, mu)
}
for _ = range names {
<-end
}
}
上述代码中需要注意两点:
A,B,C
这样的顺序,所以需要在64~67
行加一个判断,程序刚开始执行时如果获得的锁不对,就不执行任何操作,重新获得锁。60~63
行上锁之后加一个判断,保证i
的值是最新的值。除了自己手动加锁外,我们也可以使用 Go 的 atomic
包中提供的原子操作来完成上述功能。
每个 Goroutine 原子性地获得i
的值,如果符合i % 3 == threadNum
的条件,则执行操作,否则作自旋。代码如下:
package main
import (
"fmt"
"sync/atomic"
)
var (
end = make(chan struct{})
i int32
)
func threadPrint(threadNum int32, threadName string) {
for {
v := atomic.LoadInt32((*int32)(&i))
if v >= 30 {
break
}
if v%3 == threadNum {
fmt.Printf("%d: %s\n", i, threadName)
atomic.AddInt32((*int32)(&i), int32(1))
} else {
continue
}
}
end <- struct{}{}
}
func main() {
names := []string{"A", "B", "C"}
for idx, name := range names {
go threadPrint(int32(idx), name)
}
for _ = range names {
<-end
}
}
经V友 @Mark3K 的补充,还可以使用多个 channel 执行扇入(Fan In)操作,避免使用锁。
首先说一下扇入的定义,Go blog 中是这样描述的:
A function can read from multiple inputs and proceed until all are closed by multiplexing the input channels onto a single channel that's closed when all the inputs are closed. This is called fan-in.
通过将多个输入 channel 多路复用到单个处理 channel 的方式,一个函数能够从多个输入 channel 中读取数据并处理。当所有的输出 channel 都关闭的时候,单个处理 channel 也会关闭。这就叫做扇入。
在维基百科中描述的逻辑门的扇入如下(大家也可以参考这个来理解 Go 中的扇入):
Fan-in is the number of inputs a logic gate can handle. For instance the fan-in for the AND gate shown in the figure is 3. Physical logic gates with a large fan-in tend to be slower than those with a small fan-in. This is because the complexity of the input circuitry increases the input capacitance of the device. Using logic gates with higher fan-in will help reducing the depth of a logic circuit.
一个逻辑门将多个输入处理成一个输出,它能够处理的输入数量就叫做扇入。
理解了扇入的概念后,上述问题的答案也呼之欲出了。我们可以为A,B,C
三个 Goroutine 创建三个 channel。然后通过一个 FanIn 函数将三个 channel 的输出输入到一个 channel 中。具体代码如下:
package main
import "fmt"
func gen(v string, times int) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
for i := 0; i < times; i++ {
ch <- v
}
}()
return ch
}
func fanIn(times int, inputs []<-chan string) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
for i := 0; i < times; i++ {
for _, input := range inputs {
v := <-input
ch <- v
}
}
}()
return ch
}
func main() {
times := 10
inputs := make([]<-chan string, 0, 3)
for _, K := range []string{"A", "B", "C"} {
inputs = append(inputs, gen(K, times))
}
for char := range fanIn(times, inputs) {
fmt.Println(char)
}
}
1
richzhu 2019-04-07 10:15:21 +08:00 via iPhone
请教一下,为什么通道要用 struct{} 类型
|
2
bwangel OP @richzhu
字面量 struct{}代表了空的结构体类型。这样的类型既不包含任何字段也没有任何方法。该类型的值所需的存储空间几乎可以忽略不计。 因此,我们可以把这样的值作为占位值来使用。比如:在同一个应用场景下,map[int] [int]bool 类型的值占用更少的存储空间。 |
3
JaguarJack 2019-04-07 10:39:09 +08:00 via iPhone
学习了 正好看到锁这里
|
4
exiaoxing 2019-04-07 10:44:39 +08:00 via iPhone
好文,赞
|
5
xylophone21 2019-04-07 10:51:02 +08:00
这道题,感觉怪怪的。开 3 个线程,然后居然没有一点并行的需求
|
6
Mark3K 2019-04-07 10:54:50 +08:00 1
可以多开个 channel,避免加锁
func fanIn() <-chan string{ inCh := make(chan string) go func() { defer close(inCh) for i := 0; i < 10; i++{ inCh <- aCh inCh <- bCh inCh <- cCh } }() return inCh } |
7
deepdark 2019-04-07 10:55:46 +08:00 via Android
学习了,赞一个
|
8
mengzhuo 2019-04-07 11:02:59 +08:00
还自旋锁哈哈哈哈
是个正常的 Go 程序员都会选择用 fan in channel 模式的 |
10
Mark3K 2019-04-07 11:13:11 +08:00 1
@bwangel
```go package main import "fmt" func gen(v string, times int) <-chan string { ch := make(chan string) go func() { defer close(ch) for i := 0; i < times; i++ { ch <- v } }() return ch } func fanIn(times int, inputs []<-chan string) <-chan string { ch := make(chan string) go func() { defer close(ch) for i := 0; i < times; i++ { for _, input := range inputs { v := <-input ch <- v } } }() return ch } func main() { times := 10 inputs := make([]<-chan string, 0, 3) for _, K := range []string{"A", "B", "C"} { inputs = append(inputs, gen(K, times)) } for i := range fanIn(times, inputs) { fmt.Println(i) } } ``` |
11
whoisghost 2019-04-07 11:20:14 +08:00 1
V3 -- AtomicInt 版本,把 names 再追加 “ D ”, "E", 把 3 => 5, 30 => 50, 还能正常运行吗?
|
13
hjc4869 2019-04-07 11:46:51 +08:00 1
简单问题复杂化。整层楼没人提到 Semaphore 也是惊了。
|
14
jadeity 2019-04-07 12:06:28 +08:00
并行无序,阻塞有序。
需要逻辑顺序的阻塞放在主线程保证顺序,其他不需要的阻塞放在其他线程提高性能。 不管是 channel,锁,信号还是其他什么名字,在合适的地方阻塞,把不合适的阻塞并行。 |
15
yianing 2019-04-07 12:10:19 +08:00 via Android
@hjc4869 哈哈,我看到这个问题的第一想法就是操作系统中学的信号量,read copy write
|
16
bwangel OP @hjc4869 我刚刚上维基百科简单看了一下 Semaphore 的定义: https://zh.wikipedia.org/wiki/%E4%BF%A1%E5%8F%B7%E9%87%8F
感觉和我在文中写的 [正确答案 V2 – 公平锁] 的实现方式很像,可以详述一下 Semaphore 的解决方案吗?最好可以贴一些代码。 |
17
bwangel OP |
19
hjc4869 2019-04-07 12:58:39 +08:00
@bwangel
static void Worker(char c, int o, SemaphoreSlim wait, SemaphoreSlim signal, SemaphoreSlim finish) { for (int i = 0; i < 10; i++) { wait.Wait(); Console.WriteLine($"{i*3+o}: {c}"); signal.Release(); } if (finish != null) finish.Release(); } static void Main(string[] args) { SemaphoreSlim s1 = new SemaphoreSlim(0), s2 = new SemaphoreSlim(0), s3 = new SemaphoreSlim(0); SemaphoreSlim sTerm = new SemaphoreSlim(0); Task.Run(() => Worker('A', 0, s1, s2, null)); Task.Run(() => Worker('B', 1, s2, s3, null)); Task.Run(() => Worker('C', 2, s3, s1, sTerm)); s1.Release(); sTerm.Wait(); } |
20
ngn999 2019-04-07 13:02:05 +08:00
我们部门面试也问这个问题,非 go, 是考信号量
|
21
jadeity 2019-04-07 13:38:21 +08:00
@bwangel 用到并行就是为了减少阻塞,并行同步就是要阻塞。如何挑选阻塞的时机,如何更优雅更有效率的阻塞,不管怎么问,都是在加上更多限制的条件下解决这个问题。
|
22
liyunlong41 2019-04-07 13:43:06 +08:00 via iPhone
技术贴 赞一个 之前刚好看到了 go 不满足内存一致性模型,不理解,现在稍微理解了点
|
23
art2cat 2019-04-07 13:48:48 +08:00
写的很好,但是你确定不是 fairlock 吗?
|
24
whoisghost 2019-04-07 14:03:55 +08:00
@whoisghost 楼主呀,看下我这评论呀,解法 3 是不是有问题?
|
26
lazyfighter 2019-04-07 14:52:17 +08:00
在 go 语言入门的时候我记得有本书写的是能用锁的时候尽量用锁,因为 channel 还是依赖锁进行实现的
|
27
reus 2019-04-07 15:01:13 +08:00
@hjc4869 像你这样写的话,go 也不需要信号量。所以没人提信号量。
<script src="https://gist.github.com/reusee/57f30d177a688365e78ee9a12dac4468.js"></script> |
29
hjc4869 2019-04-07 16:21:46 +08:00 1
@reus 这就是把 channel 当 semaphore 用……
而且 channel 本身实现用了 mutex,mutex 底层又是 semaphore,一层层包上来不如直接用 semaphore 直观。 |
30
Coey 2019-04-07 16:22:56 +08:00 via Android
归根结底是个并行无序转为串行有序的问题,遇到这种问题我一般是觉得没什么意思
当你不真正需要并行的时候,就不要用并行 另外,我认为用 FanIn/FanOut 和其他的几个解法并不是完全等价的,原因在于它对标准输出做了串行 应该有人见过多线程环境写文件导致文件混乱的吧,噗 |
31
hjc4869 2019-04-07 16:39:30 +08:00 1
@georgetso 这个问题本质上是在实现同步,而不是互斥。同步强调的是做事的先后顺序而不是多线程访问资源的冲突。虽然二者是包含关系并且可以互相实现,但是如果这里第一眼看到题就只是想到用 lock/mutex 而不是 semaphore/channel 去实现,那么显然是对后者的应用不熟,面试要挂的……
|
32
Lpl 2019-04-07 16:56:11 +08:00
@lazyfighter 怕是在开玩笑吧?你写一个两个 goroutine 同步的程序,channel 比 mutex 快至少一倍。
|
33
georgetso 2019-04-07 17:00:04 +08:00
@hjc4869 我同意你的分析, 这不是关于互斥的问题而是关于 happens-before 的问题. 但似乎 semaphore 是为了做资源控制而不是顺序同步吧?
|
34
hjc4869 2019-04-07 17:13:34 +08:00
@georgetso Semaphore 是一种同步的手段,可以用来做很多事情。你说的资源控制是一种用途(限制 N 个并发访问),我说的用来等待 /唤醒 thread 也是一个用途。
|
36
whoisghost 2019-04-07 17:31:05 +08:00 1
|
37
Lpl 2019-04-07 17:59:23 +08:00
感觉我这个版本会更加简洁一点,本质上就是一个同步的问题。另外,mutex 和 channel 之间性能其实是有很大差别的,golang 的并发通信模型是“通信同步”( CSP )。mutex 就是单纯的一个互斥锁。
https://gist.github.com/penglongli/47395e7e75472a7da92787f3eb8e947d ``` var ( num = 10 ) func main() { sig := make(chan int) c1 := make(chan string) c2 := make(chan string) c3 := make(chan string) var wg sync.WaitGroup wg.Add(3) go print(&wg, c1, "A") go print(&wg, c2, "B") go print(&wg, c3, "C") go func() { for { select { case <-sig: // 检测到 sig 信号后退出 goroutine,避免出现 Deadlock return case str := <- c1: { fmt.Println(str) str = <- c2 fmt.Println(str) str = <- c3 fmt.Println(str) } } } }() wg.Wait() sig <- 1 } func print(wg *sync.WaitGroup, c chan string, str string) { defer wg.Done() for i := 0; i < num; i++ { c <- str } } ``` |
38
Lpl 2019-04-07 18:02:26 +08:00
这道题目有好多种解法,我又想到了那个很“经典”的睡眠排序了,用在这里很切题。。
|
39
29EtwXn6t5wgM3fD 2019-04-07 18:15:32 +08:00
经典的使用信号量实现前驱关系的题目啊。
|
40
gamexg 2019-04-07 18:19:11 +08:00
@whoisghost #36 即使加了 gosched 面试时也够呛打及格分数。
|
41
whoisghost 2019-04-07 19:03:41 +08:00
@gamexg 嗯。至少是正确的。
|
42
tairan2006 2019-04-07 19:16:44 +08:00 2
这个就是并行输出串行化…用锁写很浪费 cpu 时间或者写出惊群代码呀。
这个扇出的代码也有问题,题目要求是在各自线程里面输出,你要是这么写,不如直接从主协程里面按顺序从管道里面拿数据,更省事。我也手痒写了个玩。 https://gist.github.com/YiuTerran/f39064e3aa2152c64503a616725a65d9 |
43
index90 2019-04-07 19:28:57 +08:00
```
package main import "fmt" type Message struct { msg string ch chan bool } func T(msg string) chan Message { w := make(chan bool) c := make(chan Message) go func(){ defer close(c) for i:=0;i<10;i++ { c <- Message{msg:msg, ch:w} <-w } }() return c } func main() { A := T("A") B := T("B") C := T("C") for i:=0;i<10;i++ { a := <-A;fmt.Println(a.msg) b := <-B;fmt.Println(b.msg) c := <-C;fmt.Println(c.msg) a.ch <- true b.ch <- true c.ch <- true } } ``` 出自油管[19:21] : |
44
index90 2019-04-07 19:31:01 +08:00
补充一下,在主线程打印有点作弊,如果在子线程中打印,应该才是题目本意?
|
45
Lpl 2019-04-07 20:25:47 +08:00
没认真审题,把代码改成了单向循环链表。每个协程判断上一层的信号量 signal,接收到就打印当前节点。
https://gist.github.com/penglongli/7f8eaee1fdec19e55097bb1d041dcac3 |
46
bwangel OP |
47
reus 2019-04-07 20:58:11 +08:00
@hjc4869 你可以用“信号量”来理解,但别人未必需要。原帖 ( https://www.v2ex.com/t/547045 )几个 go 的实现,都没人提到“信号量”。
chan 还可以传递额外的信息,如果线程间不仅仅是同步而是需要传递一些数据,那单靠 semaphore 就无能为力了。go 运行时实现的 semaphore,是没有暴露到标准库的,所以没有”直接用 semaphore ”这个可选项。 |
48
realityone 2019-04-07 21:41:46 +08:00
|
49
bwangel OP |
50
tonywangcn 2019-04-07 21:58:56 +08:00
@bwangel 在 go version go1.10.3 darwin/amd64 状态下,你的代码是没有问题的。https://stackoverflow.com/questions/13107958/what-exactly-does-runtime-gosched-do
|
51
index90 2019-04-07 22:04:57 +08:00
package main
import "fmt" func T(msg string) chan struct{} { ch := make(chan struct{}) go func() { defer close(ch) for i:=0;i<10;i++ { <-ch fmt.Println(msg) ch<- struct{}{} } }() return ch } func main() { A := T("A") B := T("B") C := T("C") t := struct{}{} for ; ; { A <- t B <- <- A C <- <- B t = <- C } } |
52
myself659 2019-04-07 22:24:25 +08:00
https://gist.github.com/myself659/8fd9af077add584ec7ba0a0f86bce3b5
一个 channel 搞定 更多 channel 特性 参考一下这篇文章 https://zhuanlan.zhihu.com/p/26393117 |
53
tairan2006 2019-04-07 22:33:30 +08:00
@bwangel @tonywangcn V3 答案协程是个无阻塞的死循环啊,CPU 根本不会让渡出去…所以只能手动让出了,这个实现本身是错误的…能跑几个取决于你的 CPU 核数。
如果说同步原语的话,这题用条件变量或者信号量都可以。不过对于 golang 而言,channel 就是它的同步原语,很多时候并不一定非要用更底层的东西(过早优化是万恶之源 2333 |
54
index90 2019-04-07 22:34:27 +08:00
@myself659
编写一个程序,开启 3 个线程 A,B,C,这三个线程的输出分别为 A、B、C,每个线程将自己的 输出在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。如:ABCABCABC.... 关键点: 1. 3 个线程 2. 每个线程将自己的输出打印 3. 每个线程都打印 10 遍 所以我说,在 main 中 for 10 次,或者在 main 中打印,都应该算作弊 |
55
tairan2006 2019-04-07 22:38:24 +08:00
@myself659 大哥,人家只让创建 3 个线程…你创建 30 个协程不怕被面试官打死么
|
56
ethego 2019-04-07 22:45:40 +08:00
一个读写锁竟然能说这么啰嗦。。真理往往是简洁的
```go package main import ( "fmt" "sync" ) var mu sync.RWMutex var index int var endSignal = make(chan struct{}) func echoRoutine(routineIndex int, routineName string) { for { mu.RLock() i := index mu.RUnlock() if i >= 30 { break } if i % 3 == routineIndex { fmt.Println(i, routineName) mu.Lock() index++ mu.Unlock() } } endSignal <- struct{}{} } func main() { routineNames := []string{"A", "B", "C"} for idx, name := range routineNames { go echoRoutine(idx, name) } for _ = range routineNames { <-endSignal } } ``` |
57
tairan2006 2019-04-07 22:53:45 +08:00
|
58
myself659 2019-04-07 23:05:28 +08:00
|
60
myself659 2019-04-07 23:35:14 +08:00
|
61
myself659 2019-04-07 23:36:52 +08:00
@tairan2006
看上一楼 gist 流式处理 |
62
tairan2006 2019-04-07 23:48:31 +08:00
@myself659 Emmm …虽然思路是大体一样的,还是建议你看一下我 42 楼写的代码…因为你这个编程风格可能会让面试官感觉有点不满。。
|
64
cokeboL 2019-04-08 02:06:47 +08:00
package main
import ( "fmt" "github.com/temprory/util" "sync" ) func main() { wg := sync.WaitGroup{} cl := util.NewCorsLink("test", 10, 3) for i := 0; i < 30; i++ { idx := i wg.Add(1) cl.Go(func(task *util.LinkTask) { defer wg.Done() task.WaitPre() if idx%3 == 0 { fmt.Println("A") } else if idx%3 == 1 { fmt.Println("B") } else { fmt.Println("C") } task.Done(nil) }) } wg.Wait() cl.Stop() } |
65
ShayneWang 2019-04-08 09:21:58 +08:00
插眼
|
66
findTheWay 2019-04-08 09:43:54 +08:00
技术贴
|
67
georgetso 2019-04-08 11:02:59 +08:00
@ethego 略有异议. 该方案的确实现了既定目标, 但 cpu 空转有些过分了. 简单任务你这么写没关系, 如果是计算密集型系统, 这么写是过不了 review 的.
|
68
Gea 2019-04-08 11:23:39 +08:00
仔细看一下
|
69
ethego 2019-04-08 14:25:52 +08:00
@georgetso 什么简单不简单任务,题目的要求都清晰地摆在这里了,就不要去假设什么需求。另外这道题用原子操作并不会比上锁省多少时间,别意淫,你写出来做 benchmark 就知道了。
|
70
ethego 2019-04-08 14:33:05 +08:00
@georgetso 另外你还要再学习一个。我虽然写了 for {} 但是并没有什么 CPU 空转,mutex 没有抢到锁会阻塞等待释放,这是基础知识。
|
71
georgetso 2019-04-08 16:17:48 +08:00
@ethego 这倒是有趣了. 该学习的我虚心学习, 不过我尝试在
mu.RLock() 这一句上面添加 counter++ // (先定义一个全局变量 var counter = 0) fmt.Println(counter) 然后发现打印了 500 多行. 最后几行是: 521 522 523 524 486 29 C 526 525 518 如果 cpu 没有空转, 那不应该跑 500 多句 println 吧? 请赐教! |
72
georgetso 2019-04-08 16:21:38 +08:00
@ethego 我认真学习一个后, 认为贵码能完成任务, 主要还是靠关键的三句
mu.Lock() index++ mu.Unlock() 放在了 if 中. 这个 if 没有 else, 不过如果你把我上面回帖中的 ++ 和 print 放在这个 else 里, 就会发现 mutex 其实被抢到了 500 多次, 毕竟这是“读写锁”, 这是“基础知识”. 再请赐教! |
74
exonuclease 2019-04-08 17:46:38 +08:00
我当时实现的 c++版本是每个线程用自己的计数器来判断是否退出的
atomic_int counter = 0; const vector<char> letters = { 'A','B','C' }; void worker(int target) { int local_counter = 0; while (local_counter < 10) { if (counter.load() % 3 == target) { cout << letters[target] << endl; ++counter; ++local_counter; } } } int main() { thread a(worker, 0); thread b(worker, 1); thread c(worker, 2); a.join(); b.join(); c.join(); return 0; } |
75
ethego 2019-04-08 18:52:29 +08:00
@georgetso 当然我有误解你说的空转。不过对于这道题,你说的那点抢占开销比什么公平锁快多了。你自己动手 benchmark 试下就知道了,为了解决抢占的开销远比抢占本身大。
|
76
hheedat 2019-04-12 15:59:46 +08:00
楼主,V2 公平锁这里有个错误,贴出我的一次执行结果
``` 0: A 1: B 2: C 3: B 4: A 5: C 6: B 7: A 8: C 9: B 10: A 11: C 12: B 13: A 14: C 15: B 16: A 17: C 18: B 19: A 20: C 21: B 22: A 23: C 24: B 25: A 26: C 27: B 28: A 29: C ``` 原因在于 ```if i < 3 && i%3 != threadNum {``` 这里 应该把 i<3 这个条件去掉,你这里加这个是为了保证第一轮按照 ABC 输出,所以只在 i<3 的时候校验了,但是后面的也应该全部校验。因为即使没有收到条件变量的通知,调用其方法的 goroutine 也是有可能被唤醒的。 |
77
hheedat 2019-04-12 16:53:48 +08:00
func threadPrint(threadNum int, threadName string, mu sync.Locker) {
for i < 9 { fmt.Println("......", threadNum, threadName, "S-1") mu.Lock() if i >= 9 { mu.Unlock() continue } if i < 3 && i%3 != threadNum { fmt.Println("......", threadNum, threadName, "S-2") mu.Unlock() continue } fmt.Printf("%d: %s\n", i, threadName) i += 1 fmt.Println("......", threadNum, threadName, "S-3") mu.Unlock() } end <- struct{}{} } ...... 0 A S-1 0: A ...... 0 A S-3 ...... 0 A S-1 ...... 0 A S-2 ...... 0 A S-1 ...... 0 A S-2 ...... 2 C S-1 ...... 1 B S-1 ...... 2 C S-2 ...... 2 C S-1 1: B ...... 1 B S-3 ...... 1 B S-1 2: C ...... 2 C S-3 ...... 2 C S-1 ...... 0 A S-1 3: B ...... 1 B S-3 ...... 1 B S-1 4: C ...... 2 C S-3 ...... 2 C S-1 5: A ...... 0 A S-3 ...... 0 A S-1 6: B ...... 1 B S-3 ...... 1 B S-1 7: C ...... 2 C S-3 ...... 2 C S-1 8: A ...... 0 A S-3 打印一些状态可以看出一些端倪 |
78
hheedat 2019-04-12 17:15:29 +08:00
https://golang.org/pkg/sync/#Cond.Wait
其实还有一个问题,把 sync.Wait 用 defer 这么封装是否会有问题? wait 在阻塞的时候会先解锁,唤醒的时候会先加锁,现在唤醒之后立马就释放锁了,相当于锁的是 fl.holdCount,而不是 i,这样可能会出问题吧,i++的时候 |
79
bwangel OP @hheedat 你好,感谢你的回复,你反馈的问题确实存在。出现`BAC`的原因是这样的(为了格式更好看一些,我把内容写在了 Gist 上,希望你拥有 跨越长城 的能力):
https://gist.github.com/bwangelme/1d204647f4658007043f348a61f37936 你说的第二个问题,`i` 确实没有被 mutex 保护。但是由于每个 Goroutine 执行 `i++` 的时候都会首先获取 `holdCount` 的值,如果 holdCount 的值不为 1,那么这个 Goroutine 就会阻塞。所以可以确保同一时刻只会有一个 Goroutine 执行 `i++` |
81
hheedat 2019-04-15 09:54:26 +08:00 1
@bwangel
“你说的第二个问题,`i` 确实没有被 mutex 保护。但是由于每个 Goroutine 执行 `i++` 的时候都会首先获取 `holdCount` 的值,如果 holdCount 的值不为 1,那么这个 Goroutine 就会阻塞。所以可以确保同一时刻只会有一个 Goroutine 执行 `i++`” 不能,因为阻塞的 goroutine 可能会被误唤醒 |
83
bwangel OP @hheedat 这是修改后的代码,锁里面还是需要两个变量 holdCount 和 isHold
https://gist.github.com/bwangelme/44f0edb469733bf9bac86bbc96faf037 |