资讯一个 golang 并发的问题

2021-05-11 20:28:24 +08:00
 ng29
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
3430 次点击
所在节点    Go 编程语言
46 条回复
baiyi
2021-05-13 17:04:21 +08:00
@lesismal #20 如果考虑到其他调度的话是超出讨论范围了,我只是思考了为什么会有连续两个输出而不是交替输出的问题。我的代码中也屏蔽了其他任何可能调度的影响。

其实在真正的使用中是要避免依赖这种 runtime 的执行顺序,Go 文档中也不会提及内部的实现是有接收器 /发送器队列的。
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
2021-05-13 18:14:19 +08:00
@baiyi 如果考虑到其他调度的话是超出讨论范围了,我只是思考了为什么会有连续两个输出而不是交替输出的问题。”
—— 这个现象本身并不是 chan op 的单句代码导致的,所以你只分析 chan 内部的肯定不足够。反而正是因为其他部分代码的调度导致的现象,所以考虑其他调度也不是超出范围

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

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

你的论证是什么?你看了 fmt.Printf 函数的源码,发现确实有能主动触发调度的操作吗?还是根据现象推断的?
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
2021-05-14 12:17:59 +08:00
@baiyi
"这里只要 runtime.GOMAXPROCS 设置为 1,那么除了第一次和最后一次(如果存在的话)外,其他的输出绝对是连续两次的"
—— 这是你在 17 楼说的"绝对是两次",我举乱序的例子,是反驳你的绝对两次。同样的代码,现象已经证明你的解释是错的,你还要坚持你的解释,那我放弃跟你讨论这个问题

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

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

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

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

我杠不动 baiyi 这孩子了,你帮我劝劝他 :joy::joy:。。。 :
baiyi
2021-05-14 13:10:34 +08:00
@lesismal #28 接受批评,我有时间会再去学习研究 printf 是否会影响顺序,以及抢占式调度是否会发生的问题。
lesismal
2021-05-14 13:15:54 +08:00
@baiyi 一起学习研究,有新发现咱们继续讨论
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
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
2021-05-14 14:47:21 +08:00
@lesismal #33 我这里的代码确实有些问题,因为我要调试 chan 源码部分,所以尽量屏蔽了其他的调用,导致我没有发现乱序的存在,”绝对是连续两次“这个结论过于武断了。很抱歉。

不过去掉偶尔存在的乱序问题,连续两次的输出可以认为是 chan 等待队列机制的作用吗?
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
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
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
2021-05-14 15:16:20 +08:00
@lesismal #35 所以你也不是认为 print 影响的调度,而是说 print 前对 chan 操作并不能固定顺序,因为 runtime 有其他更多的可能性会对顺序造成影响。

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

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

我现在已经没有纠结对错了。只是我认为的是 chan 的队列机制导致的两两输出,然后你说 runtime 中可能会有其他可能对顺序造成影响,所以出现乱序。但我还是没明白为什么这就说明我的结论不能解释两两输出的现象了。

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

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

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

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

© 2021 V2EX