协程执行问题请教

2020-04-21 19:35:44 +08:00
 davidyanxw
package main

import (
"fmt"
"time"
)

var x = 0

func main() {
var num = 123
var p = &num
c := make(chan int)
go func() {
c <- *p + x // 1. 返回 123
// c <- *p // 2. 返回 789
}()
time.Sleep(time.Second)
num = 789
fmt.Println(<-c)

}

子协程,执行 1 返回 123,执行 2 返回 789
没看出来有什么差别啊?
2457 次点击
所在节点    Go 编程语言
16 条回复
kaifang
2020-04-21 19:53:05 +08:00
有区别,
1 进行了运算,取运算后的结果,你加了 1 秒的延时,很明显当时的 num 的值为 123,所以输出是 123,如果去掉延时,则为 789
2 一直指向 num 的地址,从通道里读的时候读的是 789
bwangel
2020-04-21 20:56:20 +08:00
time.Sleep 不能保证 goroutine 一定比 main 先执行,如果想要让 Goroutine 比 main 先执行的话,可以通过 waitGroup 来同步:

https://gist.github.com/bwangelme/5e71895d40130521b71828cef72adc1f
MoYi123
2020-04-21 21:21:21 +08:00
c <- *p + x // 1. 返回 123 等于 tmp:= *p + x ; c<- tmp ,因为 chan 的 size 是 0 所以要等到主进程到 fmt.Println(<-c)时才会运行 c<- tmp
blip
2020-04-22 03:04:54 +08:00
”By default channels are unbuffered, meaning that they will only accept sends (chan <-) if there is a corresponding receive (<- chan) ready to receive the sent value“
@MoYi123 正解
useben
2020-04-22 09:11:34 +08:00
@MoYi123 正解, 1 2 楼答的不是这个问题
davidyanxw
2020-04-22 13:34:42 +08:00
@blip
按照这个说法的话,应该两个值都会返回 123 才对啊?
为啥第 1 种返回 123,第 2 种返回 789
davidyanxw
2020-04-22 13:35:44 +08:00
@kaifang 1 运算了,2 为啥不运算呢?
kaifang
2020-04-22 15:30:25 +08:00
@davidyanxw #7 2,向通道发送的 num 的地址,此时通道是阻塞的,过一秒后 num 因为赋值 789 地址发生改变,然后从通道里读取 num 值是 789
blip
2020-04-23 00:11:51 +08:00
@davidyanxw good ask, 我觉得我之前没有抓住对的点,我对 unbuffered channel 的解释其实并没有回答这个问题,这里的重点其实是
1, *p+x 会 create 一个新的 int ( 123 )存放在另一个地址,这个地址存放的值没有改变过
2,*p 会一直等于 num 的地址所存储的值,这个值在代码中被更改为 789
所以才会造成两个 case 结果不同
davidyanxw
2020-04-24 11:59:32 +08:00
@blip
感谢

ch <- expression

expression 计算的时机是问题的关键。
具体的细节,希望大牛们给详细说说
guonaihong
2020-04-25 18:13:35 +08:00
这个问题和抛硬币一样。
c <-*p + x,既可能返回 123,也可能返回 789 。这和 thread 切换,先跑了哪个指令有关系(先跑了 num=789 还是后跑了),你可以把 sleep 时间修改小,证明这个现象。
```go
package main

import (
"fmt"
"time"
)

var x = 0

func call() {
var num = 123
var p = &num
c := make(chan int)
go func() {
c <- *p + x // 1. 返回 123
// c <- *p // 2. 返回 789
}()
time.Sleep(time.Second / 1000)
num = 789
readValue := <-c
if readValue == 789 {
fmt.Printf("hello 789\n")
}
//fmt.Println(readValue)
}

func main() {
for i := 0; i < 1000000; i++ {
call()
}
}

```
wsseo
2020-04-26 16:52:17 +08:00
@guonaihong 在 6c6t 的 cpu 上跑了一下,出现了一次 789 。
davidyanxw
2020-04-27 10:24:30 +08:00
@guonaihong
能详细解释下 1 这种情况既可能返回 123,也可能返回 789 吗?
wsseo
2020-04-28 14:09:09 +08:00
@davidyanxw 主协程 sleep 1s,基本本可能输出 789 了。
如果主协程 sleep 时间很短,那么 num = 789 可能会比*p + x 先执行
guonaihong
2020-04-30 18:35:07 +08:00
@wsseo @davidyanxw 这两天有点事就没有回答两位的问题。刚刚做了一些试验,发现了更有意思的地方。
1.如果写 chan 的地方是算术表达式,go 会提前进行预处理(对 c<-*p+0),可能已经换成 c<-123,所有大概率返回 123
2.如果是值,就换按正常逻辑往下走。time.Sleep 之后,两个 go 程开始了竞争,num=789 的执行速度,比唤醒生产者 chan+写数据快,所有大概率返回 789 。

当然上面从数据中总结的规律特别依赖 go 的版本(go 1.13.1),大家也不要太在意。也许哪天人家(Go Core Team)就改了。就当乐一乐,原来 go 还有一些小动作。
```go
package main

import (
"fmt"
"time"
)

var x = 0

func call() {
var num = 123
var p = &num
c := make(chan int)
go func() {
//c <- *p + 0 //大概率返回 123
c <- *p // 大概率返回 789
}()
time.Sleep(time.Second / 1000)
num = 789
readValue := <-c
if readValue == 123 {
fmt.Printf("hello %d\n", readValue)
}
}

func main() {
for i := 0; i < 1000000; i++ {
call()
}
}

```
CEBBCAT
2020-05-01 02:12:31 +08:00
俺老孙直言,V2EX 这个防 spam 系统真的是人工智障

[TIP - 如何在回复中贴代码]( /t/663565 )

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/664755

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX