疑问 没有缓存的chan在发送阻塞的时候已经把值存进去了还是在读的时候存的

a312024054 · 2017-09-03 09:12:41 · 2581 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2017-09-03 09:12:41 的主题,其中的信息可能已经有所发展或是发生改变。

package main

import (
    "fmt"
    "time"
)

func main() {
    var num = 10
    var p = &num

    c := make(chan int)

    go func() {
        time.Sleep(time.Second)
        c <- *p //-----> 11
        //c <- num //----->10
    }()

    time.Sleep(2 * time.Second)
    num++
    fmt.Println(<-c)

    fmt.Println(p)
    return
}

为什么 *p 和num的结果不一样


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2581 次点击  
加入收藏 微博
26 回复  |  直到 2017-11-18 05:16:16
channel
channel · #1 · 8年之前

你这个有 data race,结果具有不确定性

a312024054
a312024054 · #2 · 8年之前

@channel 所以两个的睡眠时间是不一样的呀

marlonche
marlonche · #3 · 8年之前
polaris
polaris · #4 · 8年之前

我跟踪了下 chan send 和 chan receive 的实现,发现 c <- *pc <- num 有如下不同:

<- 的实现 chansend 发送的值,用的数据类型是 unsafe.Pointer,分别打印 c <- *pc <- num 对应发送值的指针,发现发送 *p 时,指针直接是 num 的地址,而发送 num 时,指针是拷贝 num 之后的地址。这样一来,发送 *p 后,num 的变化会反应到 chan 接收方,而发送 num 却不会。

这里确实有点坑!

相关源码:http://docs.studygolang.com/src/runtime/chan.go 中的 chansendchanrecv

a312024054
a312024054 · #5 · 8年之前
polarispolaris #4 回复

我跟踪了下 chan send 和 chan receive 的实现,发现 `c <- *p` 和 `c <- num` 有如下不同: `<-` 的实现 `chansend` 发送的值,用的数据类型是 `unsafe.Pointer`,分别打印 `c <- *p` 和 `c <- num` 对应发送值的指针,发现发送 `*p` 时,指针直接是 num 的地址,而发送 `num` 时,指针是拷贝 num 之后的地址。这样一来,发送 `*p` 后,num 的变化会反应到 chan 接收方,而发送 `num` 却不会。 这里确实有点坑! 相关源码:http://docs.studygolang.com/src/runtime/chan.go 中的 `chansend` 和 `chanrecv`

感谢回答,我也看到这两个的源码了,但是不知道,怎么去证明,只能对这两个地方反汇编,但是水平太次,没能看懂指令集 能不能请教一下,你是怎么分别打印的? 是直接调试的go的源码? 能否指点一二

polaris
polaris · #6 · 8年之前

直接修改 go 源码,通过 print 直接输出,然后重新编译 go。

ps: 刚才你这个评论发出来两个一样的,是第一次提交提示没成功还是什么?我查查是不是有 bug。

a312024054
a312024054 · #7 · 8年之前

@polaris 没有,是我手动回复了两次 一次是引用,一次是回复,回复重了

a312024054
a312024054 · #8 · 8年之前
polarispolaris #6 回复

直接修改 go 源码,通过 print 直接输出,然后重新编译 go。 ps: 刚才你这个评论发出来两个一样的,是第一次提交提示没成功还是什么?我查查是不是有 bug。

不好意思,我想自己验证一下,但是我在编译go源码的时候 发现没有这个文件,zversion.go,我的编译环境是windows上的,并且源码是刚从github上下的,请问我应该怎么做

a312024054
a312024054 · #9 · 8年之前
a312024054a312024054 #8 回复

#6楼 @polaris 不好意思,我想自己验证一下,但是我在编译go源码的时候 发现没有这个文件,zversion.go,我的编译环境是windows上的,并且源码是刚从github上下的,请问我应该怎么做

我已经从网上查了,但并没有得到解决方法

polaris
polaris · #10 · 8年之前

go 的版本是?你之前的版本是源码安装的吗?

a312024054
a312024054 · #11 · 8年之前
polarispolaris #10 回复

go 的版本是?你之前的版本是源码安装的吗?

windows10的版本是go1.8 之前是使用二进制包安装的

marlonche
marlonche · #12 · 8年之前

这个问题很有趣,time.Sleep()并不能保证c <- *p是先于num++执行的,并且貌似没有同步的办法让c <- *p先于num++执行

marlonche
marlonche · #13 · 8年之前
package main

import (
    "fmt"
)

func main() {
    var num = 10
    var p = &num

    c := make(chan int)
    c1 := make(chan int)
    c2 := make(chan int)

    go func() {
        for {
            select {
            case c <- *p:
                return
            case c2 <- <-c1:
            }
        }
    }()

    c1 <- 1
    num++
    fmt.Println(<-c)
    fmt.Println(num)
}
a312024054
a312024054 · #14 · 8年之前
marlonchemarlonche #13 回复

package main import ( "fmt" ) func main() { var num = 10 var p = &num c := make(chan int) c1 := make(chan int) c2 := make(chan int) go func() { for { select { case c <- *p: return case c2 <- <-c1: } } }() c1 <- 1 num++ fmt.Println(<-c) fmt.Println(num) }

???

polaris
polaris · #15 · 8年之前
a312024054a312024054 #8 回复

#6楼 @polaris 不好意思,我想自己验证一下,但是我在编译go源码的时候 发现没有这个文件,zversion.go,我的编译环境是windows上的,并且源码是刚从github上下的,请问我应该怎么做

windows 下先安装 Cygwin、MinGW 或 TDM-GCC 试试,源码编译需要 gcc

a312024054
a312024054 · #16 · 8年之前
polarispolaris #15 回复

#8楼 @a312024054 windows 下先安装 Cygwin、MinGW 或 TDM-GCC 试试,源码编译需要 gcc

可以使用g1.43版的go编译go1.9 , 我已经验证你的说法了,如果是num的话,在chansend1中,elem的地址确实不是num的地址,可能是编译器做了一份拷贝之后的地址

a312024054
a312024054 · #17 · 8年之前

本问题完结

经过查看chansend1的源码,并且添加了如下注释

func chansend1(c *hchan, elem unsafe.Pointer) {
    print("chansend1: chan=", c, ", elem=", elem, "\n")
    chansend(c, elem, true, getcallerpc(unsafe.Pointer(&c)))
}

增加源代码的打印信息

fmt.Println("num=", &num)

go func() {
    time.Sleep(time.Second)

    fmt.Println("----------------")
    //c <- *p //-------> 11
    c <- num //----->10
}()

使用 c<-num 时的打印 如下

chansend1: chan=0xc042014070, elem=0x4d87b0
num= 0xc04200e0b0
----------------
chansend1: chan=0xc04203c060, elem=0xc042021fa8
10

使用c<-*p 时的打印如下

chansend1: chan=0xc042014070, elem=0x4d87b0
num= 0xc04200e0b0
----------------
chansend1: chan=0xc04203c060, elem=0xc04200e0b0
11

应该可以证明@polaris 假设是成立的

marlonche
marlonche · #18 · 8年之前

这个问题就是data race的问题,规范里面对Send statements的说明是:

Both the channel and the value expression are evaluated before communication begins

只要在通过同步方式保证c <- *p是先于num++执行的情况下,从c里面读取到的值一定是10,不管是c <- *p还是c <- num

如果不能保证c <- *pnum++的同步,结果就是不确定的,并且考虑c <- *pc <- num的结果为什么不同也没多大意义

a312024054
a312024054 · #19 · 8年之前
marlonchemarlonche #18 回复

这个问题就是data race的问题,规范里面对`Send statements`的说明是: `Both the channel and the value expression are evaluated before communication begins` 只要在通过同步方式保证`c <- *p`是先于`num++`执行的情况下,从c里面读取到的值一定是10,不管是`c <- *p`还是`c <- num` 如果不能保证`c <- *p`和`num++`的同步,结果就是不确定的,并且考虑`c <- *p`和`c <- num`的结果为什么不同也没多大意义

那请问,在同一个位置,为什么num和*p在chansend1中入参不一样

marlonche
marlonche · #20 · 8年之前

我在golang-nuts上问了这个问题,下面是一些回复:

1.

You have a data race, what value you get from dereferencing p is 
undefined, it could be 10, it could be 11, it could wipe your 
harddrive or launch the missiles. 

2.

The program is really racy, but the result is also really some counter-intuitive.
The following program also print 10, which means evaluation of pointer dereference
is some different to evaluation of other expressions in flow.

package main

import (
    "fmt"
    "time"
)

func main() {
    var num = 10
    var p = &num

    c := make(chan int)

    go func() {
        c <- func()int{return *p}() // with this line we will get 10 from channel c
        //c <- *p * 1 // with this line we will get 10 from channel c

        //c <- *p // with this line we will get 11 from channel c
        //c <- num // with this line we will get 10 from channel c
    }()

    time.Sleep(time.Second)
    num++
    fmt.Println(<-c)

    fmt.Println(p)
}

IMO, I think this is a bug.
I mean it can be viewed as a bug for inconsistency.
But from the memory model view, it can also not be viewed as a bug.
polaris
polaris · #21 · 8年之前
marlonchemarlonche #20 回复

我在golang-nuts上问了这个问题,下面是一些回复: 1. You have a data race, what value you get from dereferencing p is undefined, it could be 10, it could be 11, it could wipe your harddrive or launch the missiles. 2. The program is really racy, but the result is also really some counter-intuitive. The following program also print 10, which means evaluation of pointer dereference is some different to evaluation of other expressions in flow. package main import ( "fmt" "time" ) func main() { var num = 10 var p = &num c := make(chan int) go func() { c <- func()int{return *p}() // with this line we will get 10 from channel c //c <- *p * 1 // with this line we will get 10 from channel c //c <- *p // with this line we will get 11 from channel c //c <- num // with this line we will get 10 from channel c }() time.Sleep(time.Second) num++ fmt.Println(<-c) fmt.Println(p) } IMO, I think this is a bug. I mean it can be viewed as a bug for inconsistency. But from the memory model view, it can also not be viewed as a bug.

golang-nuts 上该问题的地址可以发下

marlonche
marlonche · #22 · 8年之前
marlonche
marlonche · #23 · 8年之前

规范里面对Send statements的说明是:

Both the channel and the value expression are evaluated before communication begins

问题的关键在于communication begins发生在什么时候?

这里有4个时间点:

A:num++

B:<-c

C: c <- *p

D: *p这个表达式求值

communication begins可能指的时间点是B或C,就是说D可能发生在goroutine里面time.Sleep(time.Second)之后,B或C之前的任意时刻

根据Go memory model,有几个happen before是可以确定的:

A在B之前,B在C之前

那么就有下面两种情况:

  1. 如果D发生在B之前,由于A也在B之前,那么不能确定A和D谁先在前
  2. 如果D发生在C之前,由于A也在C之前,那么也不能确定A和D的顺序

在A和D的执行顺序不确定的情况下,同一程序每次执行的结果都可能不同,更不要指望把D里面的*p换成num后两者执行结果每次都相同

实际应用中也不会有这样用Go用的不彻底的代码:用channel传递一个全局变量,却没有在从channel里面获取到这个变量之后再修改

Do not communicate by sharing memory; instead, share memory by communicating.

polaris
polaris · #24 · 8年之前
marlonchemarlonche #23 回复

规范里面对Send statements的说明是: Both the channel and the value expression are evaluated before communication begins 问题的关键在于`communication begins`发生在什么时候? 这里有4个时间点: A:`num++` B:`<-c` C: `c <- *p` D: `*p`这个表达式求值 `communication begins`可能指的时间点是B或C,就是说D可能发生在goroutine里面`time.Sleep(time.Second)`之后,B或C之前的任意时刻 根据Go memory model,有几个happen before是可以确定的: A在B之前,B在C之前 那么就有下面两种情况: 1. 如果D发生在B之前,由于A也在B之前,那么不能确定A和D谁先在前 2. 如果D发生在C之前,由于A也在C之前,那么也不能确定A和D的顺序 在A和D的执行顺序不确定的情况下,同一程序每次执行的结果都可能不同,更不要指望把D里面的`*p`换成`num`后两者执行结果每次都相同 实际应用中也不会有这样用Go用的不彻底的代码:用channel传递一个全局变量,却没有在从channel里面获取到这个变量之后再修改 `Do not communicate by sharing memory; instead, share memory by communicating.`

研究的很深入 :thumbsup:

a312024054
a312024054 · #25 · 7年之前
marlonchemarlonche #23 回复

规范里面对Send statements的说明是: Both the channel and the value expression are evaluated before communication begins 问题的关键在于`communication begins`发生在什么时候? 这里有4个时间点: A:`num++` B:`<-c` C: `c <- *p` D: `*p`这个表达式求值 `communication begins`可能指的时间点是B或C,就是说D可能发生在goroutine里面`time.Sleep(time.Second)`之后,B或C之前的任意时刻 根据Go memory model,有几个happen before是可以确定的: A在B之前,B在C之前 那么就有下面两种情况: 1. 如果D发生在B之前,由于A也在B之前,那么不能确定A和D谁先在前 2. 如果D发生在C之前,由于A也在C之前,那么也不能确定A和D的顺序 在A和D的执行顺序不确定的情况下,同一程序每次执行的结果都可能不同,更不要指望把D里面的`*p`换成`num`后两者执行结果每次都相同 实际应用中也不会有这样用Go用的不彻底的代码:用channel传递一个全局变量,却没有在从channel里面获取到这个变量之后再修改 `Do not communicate by sharing memory; instead, share memory by communicating.`

之前一直没看懂,今天在手机上看到以一篇博客突然很有感触,回看你的回复,确实分析的很细,回去用电脑细看,感谢

marlonche
marlonche · #26 · 7年之前

规范里面对Send statements的说明是:

Both the channel and the value expression are evaluated before communication begins

问题的关键在于communication begins发生在什么时候?

这里有4个时间点:

A:num++

B1:<-c begin

B2: <-c return

C1: c <- *p begin

C2: c <- *p return

D: *p这个表达式求值

根据Go memory model,有几个happen before是可以确定的:

C1在B2之前,B2在C2之前, A在B1之前,B1在B2之前

C1--------B2-----C2

      |

A---B1----

communication begins的时间点应该是B1与B2之间,并且同时也是C1与C2之间,按上图就是:max(B1, C1)到B2之间的一个时间点,假设为X

就是说D发生在goroutine里面time.Sleep(time.Second)之后,X之前

那么就有下面两种情况:

如果B1>C1(C1发生在B1之前),那么X在B1与B2之间,A一定在X之前,但是D也在X之前,A和D的顺序不能确定

如果C1>B1(B1发生在C1之前),那么X在C1与B2之间,A一定在X之前,但是D也在X之前,A和D的顺序不能确定

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传