单通道中Go协程解除阻塞的顺序

9
Goroutines在通道上阻塞的顺序是否决定它们解除阻塞的顺序?我不关心发送的消息的顺序(它们保证是有序的),而是关心解除阻塞的Goroutines的顺序。假设有一个空通道`ch`,多个Goroutines(1、2和3)之间共享`ch`,每个Goroutine都试图从`ch`接收一条消息。由于`ch`是空的,每个Goroutine将被阻塞。当我向`ch`发送一条消息时,Goroutine 1会首先解除阻塞吗?还是2或3可能会先接收到第一条消息?(或者反之,Goroutines尝试发送)我有一个playground,看起来表明Goroutines阻塞的顺序就是它们解除阻塞的顺序,但我不确定这是否是因为实现而导致的未定义行为。
3个回答

4
这是一个好问题 - 当进行并发设计时,它涉及到一些重要的问题。正如已经说明的那样,根据当前实现,对你具体问题的答案是基于FIFO的。除非实施者认为LIFO出于某种原因更好,否则这不太可能会改变。
但是,没有保证。因此,您应避免创建依赖于特定实现的代码。
更广泛的问题涉及到非确定性公平性饥饿
也许令人惊讶的是,在基于CSP的系统中,非确定性并不来自并行发生的事情。它是由于并发而可能发生的,但并不是因为并发而发生。相反,当做出选择时,非确定性就会产生。在CSP的正式代数中,这是通过数学建模的。幸运的是,您不需要知道数学才能使用Go。但是从正式上讲,两个goroutines可以并行执行,结果仍然可能是确定的,前提是消除了所有选择。
Go允许通过select显式地引入非确定性和通过共享通道末端隐式地引入非确定性。如果您有点对点(一个读者,一个写者)通道,则不会出现第二种情况。因此,如果在特定情况下很重要,则可以进行设计选择。
公平性和饥饿通常是同一问题的两个相反方面。饥饿是那些动态问题之一(以及死锁、活锁和竞态条件),可能导致性能不佳,更可能导致错误行为。这些动态问题无法测试(更多信息),需要一定程度的分析来解决。显然,如果系统的某个部分由于无法访问某些资源而无响应,则需要更公正地管理这些资源。

共享通道端点的访问可能会提供一定程度的公平性,因为当前的FIFO行为,这似乎已经足够了。但是,如果您想要保证(无论实现不确定性如何),可以改用select和通道束在数组中的一组点对点通道。始终首选它们并将最后选择的放在堆栈底部的顺序可以轻松实现公平索引。这个解决方案可以保证公平,但可能会带来一些性能损失。

(附言:请参见"Wot No Chickens",了解坎特伯雷研究人员发现的有关Java虚拟机中的公平性缺陷的一些有趣发现 - 这从未得到纠正!)


这是一篇真正酷炫的文章,但我通常不会鼓励人们去编写一个应用程序级别的保证公平调度器,除非他们确实有必要。在使用select时更要谨慎。 - twotwotwo
你说得没错,使用一个简单的共享通道确实更容易,因此也更少出错。我也鼓励人们通常使用这种方式。但是,了解到 select 可以在需要时使用并且知道如何进行公平选择也是一项有用的技能,这并不会有什么坏处。 - Rick-777
这就像有人问地图键是否有定义的顺序,我们自愿回答说他们可以实现自己的哈希映射表。他们可以!但首先他们需要知道内置函数确实不适用于他们,以及如何做得更好。从一开始就鼓励自定义调度会导致许多没有实际调度器引起困难的人编写可能会真正伤害到程序的代码。 - twotwotwo
无论如何,如果我在写这篇文章,我会提到应用空间调度程序的可能性,但不会鼓励在看到解决方案之前就这么做,也不会称之为“有保证的公平调度”(因为这会让人们想起例如内核调度程序,它们可以提供更强的等时保证,因为它们具有更多的控制)。 - twotwotwo

2
我认为这是未指定的,因为内存模型文档只说“通道上的发送发生在相应的接收完成之前。”发送语句接收运算符的规范部分并没有说明什么会先解除阻塞。目前,gc工具链使用有序的FIFO队列来控制哪个goroutine解除阻塞,但我没有看到规范中有任何必须始终如此的承诺。
(只是作为一般背景说明,Playground代码在GOMAXPROCS=1的情况下运行,即在一个核心上运行,因此某些类型的并发相关的不确定性不会出现。)

1

顺序未指定,但当前的实现使用先进先出队列来等待goroutines。

权威文件是Go Memory Model。内存模型不为发送到同一通道的两个goroutine定义happens-before关系,因此顺序未指定。接收也是如此。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接