V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
ng29
V2EX  ›  Go 编程语言

资讯一个 golang 并发的问题

  •  
  •   ng29 · 2021-05-11 20:28:24 +08:00 · 3392 次点击
    这是一个创建于 1053 天前的主题,其中的信息可能已经有所发展或是发生改变。
    func main() {
    	runtime.GOMAXPROCS(1)
    	ch := make(chan int)
    	go count(ch, 10000)
    	go count(ch, 10001)
    	time.Sleep(10000 * time.Millisecond)
    	fmt.Printf("exit\n")
    }
    
    func count(r chan int, who int) {
    	for {
    		if who%2 == 0 {
    			r <- who
    			fmt.Printf("|write <- who|%d\n", who)
    		} else {
    			<-r
    			fmt.Printf("| <-r recv|%d\n", who)
    		}
    	}
    }
    
    输出是
    | <-r recv|10001
    | <-r recv|10001
    |write <- who|10000
    |write <- who|10000
    为什么不是一个一个交替的形式
    | <-r recv|10001
    |write <- who|10000
    | <-r recv|10001
    |write <- who|10000
    
    46 条回复    2021-05-14 22:04:31 +08:00
    joesonw
        1
    joesonw  
       2021-05-11 20:46:52 +08:00
    cpu 太快, i/o 太慢
    AngryPanda
        2
    AngryPanda  
       2021-05-11 20:48:56 +08:00 via iPhone
    chan 操作和 printf 不是原子操作
    GoLand
        3
    GoLand  
       2021-05-11 21:51:37 +08:00
    两个 goroutine 谁先“抢到” processor 谁运行,至于这个谁先抢到谁后抢到,是调度器决定的,和你代码谁先 go 没关系,不能用简单的同步思维去预期结果。
    BeautifulSoap
        4
    BeautifulSoap  
       2021-05-11 22:34:22 +08:00
    你不能用 sync 的想法去想这种 async 问题,go 的协程和通道之后的代码你是不能确定它们什么时候会被执行的。通道虽然能保证数据一入一出,但是并不能保证塞入数据之后的代码一定会比接收数据之后的代码先执行

    一个例子
    # goroutine 1
    ch <- 100
    // code 1

    # goroutine 2
    data := <- ch
    // cdoe 2

    在这里,通道能确保 ch <- 100 比 data <- ch 先执行,但是并不能保证 // code 1 比 // code 2 先执行。这点要搞清楚

    以及 lz 的代码无论是在我的 windows 还是 wsl2 还是 goplay ground,结果都是

    |write <- who|
    | <-r recv|
    | <-r recv|
    |write <- who|
    no1xsyzy
        5
    no1xsyzy  
       2021-05-11 22:43:47 +08:00   ❤️ 4
    上面都不看 runtime.GOMAXPROCS(1) 的吗?
    这种情况下两个 goroutine 都靠同一个 chan 形成同步,是完全稳定的输出,毫无竞态(当然,写出来的代码如果依赖这一行为确实是不好的)
    而且 chan 和 printf 都是原子的,golang 默认没有 buffer 。

    一句话解释:因为是非抢占式调度。阻塞会立刻转移数据但不会提前取走调度权。每次某个 goroutine 先运行一轮满足另一 goroutine 的阻塞,再运行一轮到自己再阻塞,这才能顺利移交调度权。

    长解释:数字太长了我就写 0 和 1 了。先起了两个 go routine:0 和 1 但不会运行,因为 main() 还没交出调度权,直到 time.Sleep 才交出调度权。
    因为某种实现细节,playground 先执行了 1 (竟然是先入后出?),运行到 <- r,阻塞,交出调度权。
    这时候 0 开始运行,到 r<-who 塞进去了以后这个数据立刻被 1 拿走,但这时候 0 还没交出调度权,继续运行,运行到第二次 r<-who 的时候阻塞,交出调度权。
    这时候发现 1 可以运行了,就运行 1,先把上次阻塞的 <- r 处理了,运行一遍,再拿掉 0 阻塞着的那个 who,再运行一遍再阻塞。
    nguoidiqua
        6
    nguoidiqua  
       2021-05-12 00:15:35 +08:00
    要在通道后进行手动移交调度,才能达到你的想法。

    可以在 r <- who 或者 <- r 前面打印下标识,研究下调度问题,根据我的观察来看和 #no1xsyzy 楼说的一致。

    阻塞的读取方可以立即读到写入通道的数据(但不会同时移交调度给读取方),所以每一轮其实可以发送两个数据,一个直接给阻塞的读取方读了,一个阻塞在通道,如果通道缓存为 1,那么就是一轮三个,以此类推。
    BeautifulSoap
        7
    BeautifulSoap  
       2021-05-12 00:42:57 +08:00
    @no1xsyzy
    啊,多谢老哥提醒。有段时间没用通道都忘了通道是执行到下一次通道操作再阻塞而不是结束当前代码块的执行后阻塞了
    goushenggege
        8
    goushenggege  
       2021-05-12 09:14:55 +08:00
    gmp 模型了解下;老版本好像是可以交替,新版后变了
    ng29
        9
    ng29  
    OP
       2021-05-12 09:46:01 +08:00
    @no1xsyzy 解释很详细,感谢
    index90
        10
    index90  
       2021-05-12 11:36:37 +08:00
    @no1xsyzy 不对吧,楼主说的是先输出两个 1,再输出两个 0 。
    如果先输出两个 1,代表 chan 已经接收了两次,代表 0 也成功发送了两次,但无法解释,返送两次之间的 print 没有输出。
    index90
        11
    index90  
       2021-05-12 11:39:01 +08:00
    @no1xsyzy 我知道了,楼主的输出没有写全,正确的是:
    |write <- who|10000
    | <-r recv|10001
    | <-r recv|10001
    |write <- who|10000
    |write <- who|10000
    lesismal
        12
    lesismal  
       2021-05-12 14:03:58 +08:00
    @no1xsyzy 这个解释是错误的

    “上面都不看 runtime.GOMAXPROCS(1) 的吗?”
    —— 跟 runtime.GOMAXPROCS(1) 没关系,尝试下 runtime.GOMAXPROCS(大于 1),也一样会出现两两一组的日志

    “一句话解释:因为是非抢占式调度。”
    —— golang 好像是 1.2 版中开始引入比较初级的抢占式调度,然后好像是 1.14 做得更彻底,即使 for{} 也能释放调度权了

    这个代码的 chan 虽然是无缓冲的,但只能保证 chan 的 send 和 recv 单句代码的原子性,记得那句话吗——不要通过共享内存来通信,而应该通过通信来共享内存。这句话主要是指用 chan 来保证内存的一致性,因为传统的方法用锁、锁在复杂的业务场景更烧脑并且一不小心容易死锁

    用 chan 保证内存的一致性,进一步就可以做到一些业务逻辑的串行化,通常用 chan 也主要是用来做内存读写和逻辑的串行化从而保证一致性,但这并不是承诺多个协程同一个 chan 前后代码段的所有代码执行顺序

    楼主代码中的实验是用 printf 打印,而 printf 本身就可能触发调度权的出让,所以其实现象不是 chan 直接导致的,而是由于 printf 时的出让顺序导致的

    举个例子(对应代码注释中的 1 、2 、3 、4 ):

    func count(r chan int, who int) {
    for {
    if who%2 == 0 {
    r <- who // 1
    fmt.Printf("|write <- who|%d\n", who) // 2
    } else {
    <-r // 3
    fmt.Printf("| <-r recv|%d\n", who) // 4
    }
    }
    }

    1 执行后立刻执行了 2,打印了 write,出让
    3 执行,4 执行前出让
    又执行了一组 1 、2,再次打印了 write,出让
    4 继续执行,打印了一个 recv,出让
    1 执行,2 执行前出让调度
    3 执行,4 执行,再次打印了一个 recv
    ...
    依次类推,每次 printf 前都可能出让

    顺便宣传下自己两个项目,欢迎来玩玩
    https://v2ex.com/t/755862
    lesismal
        13
    lesismal  
       2021-05-12 14:08:22 +08:00
    @no1xsyzy
    "而且 chan 和 printf 都是原子的,golang 默认没有 buffer 。"
    —— 跟 printf 有没有 buffer 也没关系,即使有 buffer,先调用 printf 的也是先入 buffer
    tairan2006
        14
    tairan2006  
       2021-05-12 14:32:48 +08:00
    现在版本已经实现了抢占式调度……

    不建议研究这些东西,语言内部实现机制本来就会变化,各版本之间可能毫不兼容…调度器后面肯定还会再优化。

    你要是对这些感兴趣不如研究 C++,undefined behavior 一堆。
    baiyi
        15
    baiyi  
       2021-05-13 09:37:43 +08:00
    这主要是由于 channel 的内部实现机制,channel 的 send 或 recv 操作在发现有等待中的接收器( chan.recvq )或发送器( chan.sendq )时,会直接把值交给它,并向其设置为下一个要唤醒的 goroutine 。然后在循环中再次的操作,才会阻塞,阻塞时自己也会在相应的等待队列中。所以每次的 send 或 recv 操作,都会执行两次,输出的形式也是连着输出,但输出和访问 send 和 recv 函数的顺序是不一样的,因为存在阻塞。

    这个是比较直观一些的代码,因为调度不一致,所以我用 runtime.Gosched() 方法保证一定是 recv 的 goroutine 先运行。
    https://play.golang.org/p/wmU0fpTt5uf

    这里是带有 Go Channel 源码部分的一些打印:

    ------------- call unbuffered channel recv ---------------- # 接收器先启动,调用 recv 时阻塞并将自己放入队列
    ------------- call unbuffered channel send ---------------- # 发送器启动,调用 send,并尝试获取
    ------------- unbuffered channel send ---------------- # 发现队列中有可用的接收器,尝试直接发送
    ------------- unbuffered channel sent ---------------- # 成功
    written0 #继续向下执行,打印 witten
    ------------- call unbuffered channel send ---------------- # for 执行到第二次,调用 send,阻塞了
    received # 发送器在发送时,直接修改了接收器的执行栈,所以接收器不需要从阻塞那里再次运行,而是得到值,并继续向下执行,也就是打印
    ------------- call unbuffered channel recv ---------------- # for 执行到第二次,调用 recv
    ------------- unbuffered channel receive ---------------- # 发现队列中有可用的发送器,所以直接取值
    ------------- unbuffered channel received ---------------- # 成功
    received # 继续向下执行,打印 received
    ------------- call unbuffered channel recv ---------------- # 再次调用 recv,但此时队列已经空了,所以阻塞
    written1 # 此处的打印是上面被阻塞的 send 的向下执行,同样发送器也不需要在阻塞处继续执行,而是向下执行,所以直接打印第二次循环的输出
    ------------- call unbuffered channel send ---------------- # 下面就不用详细讲了,都是一样的流程
    ------------- unbuffered channel send ----------------
    ------------- unbuffered channel sent ----------------
    written2
    ------------- call unbuffered channel send ----------------
    received
    ------------- call unbuffered channel recv ----------------
    ------------- unbuffered channel receive ----------------
    ------------- unbuffered channel received ----------------
    received
    ------------- call unbuffered channel recv ----------------
    written3
    ------------- call unbuffered channel send ----------------
    ------------- unbuffered channel send ----------------
    ------------- unbuffered channel sent ----------------
    written4
    received
    ------------- call unbuffered channel recv ----------------

    如果想看源码来调试或了解,推荐看这里: https://golang.design/under-the-hood/zh-cn/part1basic/ch03lang/chan/
    lesismal
        16
    lesismal  
       2021-05-13 11:16:33 +08:00
    @baiyi 这样解释应该是不对的。
    “然后在循环中再次的操作,才会阻塞,阻塞时自己也会在相应的等待队列中。所以每次的 send 或 recv 操作,都会执行两次,输出的形式也是连着输出”
    —— 你都说了,再次的操作会阻塞,比如 A 的再次操作阻塞了,然后就暂时没走到 print,需要等 B 触发 A 的运行后才能 print,也就是说,A 的两次之间,只有一次 print,然后 B 触发了之后才能再次 print,这两次中间阻塞过、并不是连着输出。

    你看 #11 的,通常一开始的时候日志是这样的,第一句他就不是连续的两次:
    |write <- who|10000 // 不是连续的两次,只有一次
    | <-r recv|10001
    | <-r recv|10001
    |write <- who|10000
    |write <- who|10000

    再举个例子,简单点,我开 4 个协程

    runtime.GOMAXPROCS(1)
    ch := make(chan int)
    go count(ch, 10000)
    go count(ch, 10001)
    go count(ch, 10002)
    go count(ch, 10003)

    然后某段日志里:

    | <-r recv|10003
    | <-r recv|10003
    | <-r recv|10001 // 不是连续的两次,只有一次
    |write <- who|10002 // 不是连续的两次而是三次
    |write <- who|10002 // 不是连续的两次而是三次
    |write <- who|10002 // 不是连续的两次而是三次
    | <-r recv|10001
    | <-r recv|10001
    |write <- who|10002
    |write <- who|10002

    runtime 的调度不可能是这样简单对 chan 稳定的两次然后就调度,即使是因为楼主代码的例子场景这两个协成比较均衡导致基本是出现两次,但这也并不是 runtime 提供的保证

    看下我 #12 楼的解释
    baiyi
        17
    baiyi  
       2021-05-13 16:14:44 +08:00
    @lesismal #16 你可以看下我的输出日志,连续两个的 print 中,第一个永远是上一次 for 的继续执行,而不是这一次 for 的两次执行中的打印,所以我说“但输出和访问 send 和 recv 函数的顺序是不一样的,因为存在阻塞”,因为两次中的后一次,要等待阻塞结束后来后才能输出。只是表现形式是连续两次。

    这里只要 runtime.GOMAXPROCS 设置为 1,那么除了第一次和最后一次(如果存在的话)外,其他的输出绝对是连续两次的,这与 goroutine 的调度没有关系,因为只有一个 p 在运行。
    也与抢占式调度没有关系,因为每次都是依靠 chan 进行手动调度。
    baiyi
        18
    baiyi  
       2021-05-13 16:19:53 +08:00
    @lesismal #16
    以 recv 举例,for 因为阻塞被断开了:
    第一次执行 0.5,阻塞。
    回来后继续执行 0.5 (输出),然后执行完整的 1 个 for (输出),然后再次执行 0.5,阻塞。

    虽然 call recv 函数的次数是 2,但因为阻塞,for 中的整体逻辑被分割了。
    lesismal
        19
    lesismal  
       2021-05-13 16:48:14 +08:00
    @baiyi 你多跑几次例子试试,至少我这里中间可以遇到这样的日志:

    | <-r recv|10001
    | <-r recv|10001
    |write <- who|10000
    |write <- who|10000
    | <-r recv|10001
    | <-r recv|10001
    |write <- who|10000 ///////////// 不是连续的两次,也不是第一次和最后一次
    | <-r recv|10001 ///////////// 不是连续的两次,也不是第一次和最后一次
    |write <- who|10000
    |write <- who|10000
    | <-r recv|10001
    | <-r recv|10001
    |write <- who|10000
    |write <- who|10000
    | <-r recv|10001
    | <-r recv|10001
    lesismal
        20
    lesismal  
       2021-05-13 16:53:53 +08:00
    @baiyi golang 的内存模型,无缓冲的 chan,比如两个 goroutine 分别 send 、recv 之间,可以保证这两个 A chan op 前段代码先于 B chan op 后段代码执行,但不能保证 A 和 B op 后段代码的执行顺序,因为 chan op 之后的代码随时也可能被调度

    比如

    goroutine A:

    some code... // 1
    chan <- v
    some code... // 3
    some code... // 4

    goroutine B:
    <-chan
    some code... // 2
    some code... // 5
    some code... // 6

    这里能保证的是 1 先于 2/5/6 执行,但是不能保证 3 和 4,因为 3 和 4 执行之前就可能被调度了
    baiyi
        21
    baiyi  
       2021-05-13 17:04:21 +08:00
    @lesismal #20 如果考虑到其他调度的话是超出讨论范围了,我只是思考了为什么会有连续两个输出而不是交替输出的问题。我的代码中也屏蔽了其他任何可能调度的影响。

    其实在真正的使用中是要避免依赖这种 runtime 的执行顺序,Go 文档中也不会提及内部的实现是有接收器 /发送器队列的。
    lesismal
        22
    lesismal  
       2021-05-13 17:35:55 +08:00
    @baiyi
    send 的时候先检查 recvq,等待队列有 waiter 的话直接发给第一个 waiter
    https://github.com/golang/go/blob/master/src/runtime/chan.go#L207
    并标记 waiter 的那个 g 为可运行状态,顺着代码往下看就是了
    https://github.com/golang/go/blob/master/src/runtime/chan.go#L320

    这里需要着重说的一点是,标记可运行不是立刻就运行,而且就算立刻运行,也不能保证 chan op 之后的一段代码全部在单次调度运行中执行完,所以你调试 chan 内部的实现逻辑,其实解释不了这个现象,解释现象,我 #12 的应该说得差不多了

    recv 的逻辑也类似,代码就不贴了
    lesismal
        23
    lesismal  
       2021-05-13 18:14:19 +08:00
    @baiyi 如果考虑到其他调度的话是超出讨论范围了,我只是思考了为什么会有连续两个输出而不是交替输出的问题。”
    —— 这个现象本身并不是 chan op 的单句代码导致的,所以你只分析 chan 内部的肯定不足够。反而正是因为其他部分代码的调度导致的现象,所以考虑其他调度也不是超出范围

    “其实在真正的使用中是要避免依赖这种 runtime 的执行顺序”
    —— 对于多数人,“是否需要依赖以及如何避免依赖 rutime 调度”本身就是个难题,楼主和很多人的意图其实应该是想依赖 chan 做流控,但是对 golang 内存模型+happens-before 与调度场景下的代码执行顺序没弄太清楚所以才会疑惑。#20 例子中的想确保 1 和 2 的顺序这种场景用 chan 还是可以的
    baiyi
        24
    baiyi  
       2021-05-14 09:35:03 +08:00
    @lesismal #22 我在 15 层的回复也解释过是设置为下一个要唤醒的 goroutine 。同时我认为你在 12 楼的解释将其认为是 printf 造成的调度我不认可,你也没有给出论证。我还是认为 chan 本身的特性所导致的,这个特性就是 chan 的等待队列可直接传值的操作。
    lesismal
        25
    lesismal  
       2021-05-14 10:41:27 +08:00
    @baiyi
    “我还是认为 chan 本身的特性所导致的,这个特性就是 chan 的等待队列可直接传值的操作。”
    —— 最简单的问题,单从现象上说, #19 的日志,你可以试一下,这能说明 chan 本身特性的解释是不对的,明明都解释不了,就没必要继续坚持了吧 :joy:

    “同时我认为你在 12 楼的解释将其认为是 printf 造成的调度我不认可,你也没有给出论证。”
    —— 论证我已经解释得很清楚了,如果这都算没论证,那我无言以对了。或者你考虑下再仔细看看我上面几楼的回复,如果哪里不对,你也可以指出来、我再琢磨琢磨。。。
    baiyi
        26
    baiyi  
       2021-05-14 11:23:40 +08:00
    @lesismal #25 #19 的日志与楼主贴出的连续执行两次的日志没有关系,楼主也没有问为什么在多次连续的操作中会有一次乱序。你引入了这个结果,又没有自己说明。或者说你认为这个结果是 fmt.Printf 函数导致,但这跟我说的有什么冲突吗?我为什么要解释这个问题,我给出的示例代码没有出现乱序的现象。

    你的论证是什么?你看了 fmt.Printf 函数的源码,发现确实有能主动触发调度的操作吗?还是根据现象推断的?
    no1xsyzy
        27
    no1xsyzy  
       2021-05-14 12:04:34 +08:00
    @lesismal 好吧,我碰的 go 还是 1.0 刚发布的时候
    竟然这样的改动没体现在大版本号上,这不 semver (

    这段日志其实并不能说明任何问题:
    | <-r recv|10003 /// 解阻塞 ? write
    | <-r recv|10003 /// 阻塞 3 read
    | <-r recv|10001 /// 阻塞 1 read
    |write <- who|10002 /// 解阻塞 3/1 read
    |write <- who|10002 /// 解阻塞 1/3 read
    |write <- who|10002 /// 阻塞 2 write
    | <-r recv|10001 /// 解阻塞 2 write
    | <-r recv|10001 /// 阻塞 1 read
    |write <- who|10002 /// 解阻塞 1 read
    |write <- who|10002 /// 阻塞 2 write
    只能看出调度和解阻塞并不是 FIFO

    恐怕主要是交错难以发现,或者说是偶然条件。在输出的地方添加了管道:
    go run main.go | uniq -c | awk '$1!="2"'
    确实发现少量人眼难以察觉的交错

    ——

    确实 printf 可能,但通常不会让出调度,这一行为我不确定原因。
    (复制一份源文件,并把 rx tx 两处 printf 都用 vim 搞个 yy7p (扩展成 8 行 printf ))
    go run manyprint.go | uniq -c | awk '$1!="16"'
    (除第一组外)输出 8 的次数都很少,输出 8 的平均个数比前一个输出 1 的平均个数都少
    lesismal
        28
    lesismal  
       2021-05-14 12:17:59 +08:00
    @baiyi
    "这里只要 runtime.GOMAXPROCS 设置为 1,那么除了第一次和最后一次(如果存在的话)外,其他的输出绝对是连续两次的"
    —— 这是你在 17 楼说的"绝对是两次",我举乱序的例子,是反驳你的绝对两次。同样的代码,现象已经证明你的解释是错的,你还要坚持你的解释,那我放弃跟你讨论这个问题

    “楼主也没有问为什么在多次连续的操作中会有一次乱序”
    —— 但是楼主问的是“为什么不是一个一个交替的形式”,我在以上好几个楼都解释过了 print 的原因,并且这个并不能保证固定的连续两次

    “你的论证是什么?你看了 fmt.Printf 函数的源码,发现确实有能主动触发调度的操作吗?还是根据现象推断的?”
    —— 你这么讲话的话,说明你根本没了解什么是抢占式调度、go 什么时候可能发生调度(我之前楼层也有提过一点),那我只能怀疑你看不懂我说的了,那就没必要再聊了,这个相关的资料一搜大把,去找资料先看一下吧。。。

    从你回复的分析中能看的出,你算是个能钻研的娃,一般人不会去啃源码。但人年轻气盛的时候,可能聪明反被聪明误,因为觉得自己具备多数人不具备的源码阅读调试能力和钻研精神、并且在源码中窥探读懂了一些,所以更偏执于自己是正确的、可能会在对错上纠结、听不进去别人说什么,这个问题,我建议是冷静一下过几天你再来仔细研究下吧,我的回复已经足够详细了,如果你认为哪里有错误可以指出,我也会虚心继续研究

    我也年轻过,但是接触得越多,越会明白自己还很菜,所以技术问题,心态平和些
    lesismal
        29
    lesismal  
       2021-05-14 12:23:24 +08:00
    @no1xsyzy 我找这个实际的日志例子是为了说 baiyi 的分析存在的问题,其实我第一次回复中的解释已经算比较清楚了,1.14 之后的抢占式,随时可能调度,所以在 chan send 和 recv 后面代码段之间的并发 print 的顺序是无法保证的,交替各一次和各连续两次都没法保证

    我杠不动 baiyi 这孩子了,你帮我劝劝他 :joy::joy:。。。 :
    baiyi
        30
    baiyi  
       2021-05-14 13:10:34 +08:00
    @lesismal #28 接受批评,我有时间会再去学习研究 printf 是否会影响顺序,以及抢占式调度是否会发生的问题。
    lesismal
        31
    lesismal  
       2021-05-14 13:15:54 +08:00
    @baiyi 一起学习研究,有新发现咱们继续讨论
    baiyi
        32
    baiyi  
       2021-05-14 14:09:05 +08:00
    @lesismal #31 你好,我又调试研究了一下,我认为我们之间的主要在于调用的 print 函数不同。我在我的示例代码( https://play.golang.org/p/wmU0fpTt5uf )中尽量屏蔽了其他可能对输出顺序造成影响的函数,所以使用了 print 语句,而不是楼主原有的 fmt.Printf 函数。
    结果还是连续两次的输出,而不是交替的输出,并且通过对 chan 源码的调试,能证明确实有连续两次的调用。这是否足以说明不是 print 的影响造成的连续两次输出。

    关于抢占式调度是我原来理解错误,我原来想当然的认为在 goroutine 能够主动出让调度的情况下,sysmon 并不会抢占。然而阻塞并不会刷新 goroutine 的运行时间,还是会被抢占。
    不过我认为这并不会影响我上面的结论。

    ps: 我在我的示例代码中也使用 fmt.Print 函数后,发现确实经常输出乱序的结果,这应该是其内部机制造成的,但我没有仔细研究。
    lesismal
        33
    lesismal  
       2021-05-14 14:25:06 +08:00
    @baiyi 两个问题:

    1. 分析楼主的问题,当然应该尽量用楼主相同的代码好些 :smile: :smile:

    2. 你这里的例子循环次数只有 5,数量太少可能观察不到,修改下就来 10 秒的,你试试这个
    package main

    import (
    "runtime"
    "time"
    )

    func main() {
    runtime.GOMAXPROCS(1)
    ch := make(chan int)
    go func() {
    runtime.Gosched()
    for i := 0; true; i++ {
    ch <- 100
    print("written", i, "\n")
    }
    }()
    var ep = 100
    go func() {
    for {
    ep = <-ch
    print("received\n")
    }
    }()
    time.Sleep(10 * time.Second)
    }

    然后再统计下( print 好像是直接 stderr 的,所以重定向下):
    go run main.go 2>&1 | uniq -c | awk '$1!="2"'
    或者 > x.log 日志文件你自己再搜下,应该就可以发现有不是连续两次的,我这里已经有只一次的日志产生
    baiyi
        34
    baiyi  
       2021-05-14 14:47:21 +08:00
    @lesismal #33 我这里的代码确实有些问题,因为我要调试 chan 源码部分,所以尽量屏蔽了其他的调用,导致我没有发现乱序的存在,”绝对是连续两次“这个结论过于武断了。很抱歉。

    不过去掉偶尔存在的乱序问题,连续两次的输出可以认为是 chan 等待队列机制的作用吗?
    lesismal
        35
    lesismal  
       2021-05-14 15:01:34 +08:00
    @baiyi 咱们再看下 print 的源码

    ```golang
    package main

    func main() {
    print("hello world")
    }
    ```

    print 是 buildin,对应的汇编源码:

    ```sh
    go tool compile -S .\print.go > print.s
    ```

    有点长,只看 print 的部分:

    ```asm
    0x0024 00036 (.\print.go:4) CALL runtime.printlock(SB) // 加锁
    0x0029 00041 (.\print.go:4) LEAQ go.string."hello world %d, %s\n"(SB), AX
    0x0030 00048 (.\print.go:4) MOVQ AX, (SP)
    0x0034 00052 (.\print.go:4) MOVQ $19, 8(SP)
    0x003d 00061 (.\print.go:4) NOP
    0x0040 00064 (.\print.go:4) CALL runtime.printstring(SB)
    0x0045 00069 (.\print.go:4) MOVQ $1, (SP)
    0x004d 00077 (.\print.go:4) CALL runtime.printint(SB)
    0x0052 00082 (.\print.go:4) LEAQ go.string."hi"(SB), AX
    0x0059 00089 (.\print.go:4) MOVQ AX, (SP)
    0x005d 00093 (.\print.go:4) MOVQ $2, 8(SP)
    0x0066 00102 (.\print.go:4) CALL runtime.printstring(SB)
    0x006b 00107 (.\print.go:4) CALL runtime.printunlock(SB) // 解锁
    ```

    print 执行过程中是对本 m 加了锁的,即使是 runtime.GOMAXPROCS(1),也能保证 print 先后的顺序:
    https://github.com/golang/go/blob/master/src/runtime/print.go#L66
    https://github.com/golang/go/blob/master/src/runtime/print.go#L76

    而即使加了锁,依然会出现非固定的两两一组或者交替,说明这并不是进入 print 后造成的,所以即使是源码分析,也跟直接 print 还是 fmt 的 print 系列没关系
    我前面说的 print,都是说 print 之前就可能被调度了,其实都是调度器决定的,而调度器并不能保证这些固定的顺序
    lesismal
        36
    lesismal  
       2021-05-14 15:08:01 +08:00
    @baiyi
    “不过去掉偶尔存在的乱序问题,连续两次的输出可以认为是 chan 等待队列机制的作用吗?”
    —— 你搜下 golang 内存模型、happens before,结合 #20 的例子,其实这个是对 chan 在要求时序场景用法的误解,保证内存读写顺序 /临界区顺序,跟多个并发流非锁定(包括类似#20 用 chan 做类似的穿行方式)区域内代码段调度顺序是两码事。
    如果想明白了,你就能理解其实这个现象跟 chan 没直接关系,你只要思考代码段、调度就行了:楼主代码里的 chan send 和 recv 后面直到下次循环 chan recv 阻塞之前的代码段,其实都是无串行化的两个或者多个并发流,这些代码段(相当于#20 里的 [3->4->1] 与 [2->5->6],这两个过程中互相没影响没有被串行化),并不受 chan 内部实现逻辑的影响,而是被调度器决定运行时机
    lesismal
        37
    lesismal  
       2021-05-14 15:12:14 +08:00
    @baiyi
    “不过去掉偶尔存在的乱序问题,连续两次的输出可以认为是 chan 等待队列机制的作用吗?”
    —— 你搜下 golang 内存模型、happens before,结合 #20 的例子,其实楼主这个例子是对 chan 在要求时序场景用法的误解,[保证内存读写顺序 /临界区顺序] 跟 [多个并发流非锁定(包括类似#20 用 chan 做类似的穿行方式)区域内代码段调度顺序] 是两码事。
    如果想明白了,你就能理解其实这个现象跟 chan 没直接关系,你只要思考代码段、调度就行了:楼主代码里的 chan send 和 recv 后面直到下次循环 send recv 阻塞之前的代码段,其实都是无串行化的两个或者多个并发流,这些代码段(相当于#20 里的 [3->4->1] 与 [2->5->6],这两个过程中互相没影响没有被串行化),并不受 chan 内部实现逻辑的影响,而是被调度器决定运行时机

    上一楼怕有误解,编辑下,v 站这个不能编辑确实难受 :joy:
    baiyi
        38
    baiyi  
       2021-05-14 15:16:20 +08:00
    @lesismal #35 所以你也不是认为 print 影响的调度,而是说 print 前对 chan 操作并不能固定顺序,因为 runtime 有其他更多的可能性会对顺序造成影响。

    我之前大部分的时间都是思考为什么两两输出,没有考虑到更多其他变量对顺序造成的影响。再加上我之前的表述可能也有问题。
    所以我们的结论并不是矛盾的
    lesismal
        39
    lesismal  
       2021-05-14 15:27:42 +08:00
    @baiyi 所以我让你看调度的资料,#12 就说过了:
    “—— golang 好像是 1.2 版中开始引入比较初级的抢占式调度,然后好像是 1.14 做得更彻底,即使 for{} 也能释放调度权了”

    “所以我们的结论并不是矛盾的”
    —— 想啥呢,你的解释跟实际现象都不一样了。。先去查资料,去分析为什么会这样、不要纠结于自己分析的对错,纠结对错就被自己陷住了、会不自觉地想往自己是合理的方向上靠、然后失去理智分析的判断力
    baiyi
        40
    baiyi  
       2021-05-14 15:37:28 +08:00
    @lesismal #39 抢占式调度我的理解之前确实有些问题,现在我也知道我的示例代码也会被抢占了。

    我现在已经没有纠结对错了。只是我认为的是 chan 的队列机制导致的两两输出,然后你说 runtime 中可能会有其他可能对顺序造成影响,所以出现乱序。但我还是没明白为什么这就说明我的结论不能解释两两输出的现象了。
    lesismal
        41
    lesismal  
       2021-05-14 16:15:46 +08:00
    @baiyi 其实就两点:
    1. #20 里的 [3->4->1] 与 [2->5->6],这两段不包含 chan 的过程互相没影响的两个并发流,所以,跟 chan 没关系
    2. 抢占式,随时可能被调度,跟是不是 print 也没关系

    chan 和 print 都不是影响实际运行时的调度的充要条件,如果你多加一些 print,除了各自 goroutine 内的顺序能保证,多个 goroutine 之间的顺序没法保证
    chan 、print 或者其他语句,是被调度器决定他们的执行权,他们反过来只能影响调度器对调度时间等的累计、调度时间的分配,但只是影响,影响是“可能会怎样”而不是“必然会怎样”
    lesismal
        42
    lesismal  
       2021-05-14 16:19:26 +08:00
    @baiyi 你相当于是用可能性去解释必然性,所以会懵 :joy: :joy:
    lesismal
        43
    lesismal  
       2021-05-14 16:22:01 +08:00   ❤️ 1
    @baiyi 放松一下,出门转转透透气,喝点茶水、咖啡,眺望下远方,过阵子再想,就豁然开朗了。有时候思考比较绕的问题会懵住,我也经常,有时候要一个问题纠结几天想不明白,然后放下了,突然某个时候又想起来、灵光一闪,灯火阑珊的感觉
    baiyi
        44
    baiyi  
       2021-05-14 17:06:11 +08:00
    @lesismal #43 我明白你的意思,我现在已经不再认为我的示例代码中的顺序是一致的了,chan 也只是将接收器 /发送器标记为下一个要运行的 goroutine,但不能保证其一定是下一个运行的。哪怕是在我的示例代码中尽可能少的影响因素也是一样,因为还有抢占式调度。
    我现在的结论也只是从 chan 的运行逻辑上解释两两输出的可能性,但 chan 不能必然导致两两输出。

    其实结论已经不重要了,谢谢你帮我认识到我对于 go runtime 理解的不足。
    lesismal
        45
    lesismal  
       2021-05-14 20:04:27 +08:00
    @baiyi 你的钻研精神非常棒,很赞,继续加油!
    bigNewsMaker
        46
    bigNewsMaker  
       2021-05-14 22:04:31 +08:00 via iPhone
    楼上二位 V 站之光,赞
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2907 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 14:15 · PVG 22:15 · LAX 07:15 · JFK 10:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.