在选择时,当多个事情同时发生时,预期的行为是什么?

huangapple go评论81阅读模式
英文:

Expected behavior when multiple things happen together in select

问题

假设有一个goroutine正在等待以下两个无缓冲通道onetwo的select语句:

select {
    case <-one:
        fmt.Println("read from one")
    case <-two:
        fmt.Println("read from two")
}

还有一个goroutine正在等待以下发送操作:

one <- 1

另一个goroutine正在等待以下发送操作:

two <- 2

第一个等待select语句的goroutine意味着两个通道onetwo的缓冲区中都有空间,那么哪个select case是保证会执行的?它是确定性的,还是可以任意选择一个通道并在最后留下一个未读的值?

如果只有一个确定的输出,那么select是否确保对参与select的所有通道的所有操作都有一个总体顺序?这似乎非常低效...


例如,在以下代码中:

package main

import (
    "fmt"
    "time"
    "sync"
)

func main() {
    one_net := 0
    two_net := 0
    var mtx = &sync.Mutex{}
    for i := 0; i < 8; i++ {
        one, two := make(chan int), make(chan int)
        go func() { // go routine one
            select {
            case <-one:
                fmt.Println("read from one")

                mtx.Lock()
                one_net++
                mtx.Unlock()
            case <-two:
                fmt.Println("read from two")

                mtx.Lock()
                two_net++
                mtx.Unlock()
            }
        }()
        go func() { // go routine two
            one <- 1

            mtx.Lock()
            one_net--
            mtx.Unlock()

            fmt.Println("Wrote to one")
        }()
        go func() { // go routine three
            two <- 2

            mtx.Lock()
            two_net--
            mtx.Unlock()

            fmt.Println("Wrote to two")
        }()
        time.Sleep(time.Millisecond)
    }
    mtx.Lock()
    fmt.Println("one_net", one_net)
    fmt.Println("two_net", two_net)
    mtx.Unlock()
}

在最后,读取的次数和写入的次数是否可能不匹配(即one_nettwo_net是否可能非零)?例如,在select语句等待从两个通道读取的情况下,然后goroutine two和three执行各自的写入操作,但是select只选择其中一个写入操作。

英文:

Assuming one goroutine is waiting on the following select on two unbuffered channels one and two

select {
    case &lt;-one:
        fmt.Println(&quot;read from one&quot;)
    case &lt;-two:
        fmt.Println(&quot;read from two&quot;)
}

and one one goroutine is waiting on the following send

one &lt;- 1

and another is waiting on the following

two &lt;- 2

The first waiting on a select implies that there is room in the buffer for both the channels one and two, then which select case is guaranteed to run? Is it deterministic or can either run with one channel left with one unread value at the end.

If there is only one guaranteed net output, then do selects ensure a total order across all operations on all the channels participating in the select? That seems very inefficient..


For example in the following code

package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
    &quot;sync&quot;
)

func main() {
    one_net := 0
    two_net := 0
    var mtx = &amp;sync.Mutex{}
    for i := 0; i &lt; 8; i++ {
        one, two := make(chan int), make(chan int)
        go func() { // go routine one
            select {
            case &lt;-one:
                fmt.Println(&quot;read from one&quot;)

                mtx.Lock()
                one_net++
                mtx.Unlock()
            case &lt;-two:
                fmt.Println(&quot;read from two&quot;)

                mtx.Lock()
                two_net++
                mtx.Unlock()
            }
        }()
        go func() { // go routine two
            one &lt;- 1

            mtx.Lock()
            one_net--
            mtx.Unlock()

            fmt.Println(&quot;Wrote to one&quot;)
        }()
        go func() { // go routine three
            two &lt;- 2

            mtx.Lock()
            two_net--
            mtx.Unlock()

            fmt.Println(&quot;Wrote to two&quot;)
        }()
        time.Sleep(time.Millisecond)
    }
    mtx.Lock()
    fmt.Println(&quot;one_net&quot;, one_net)
    fmt.Println(&quot;two_net&quot;, two_net)
    mtx.Unlock()
}

can there even be a mismatch in the number of reads vs the number of writes (i.e. can one_net and two_net be non 0 at the end)? For example in the case where the select statement is waiting on a read from both channels, and then goroutines two and three go through with their respective writes, but then the select only picks up on one of those writes.

答案1

得分: 2

《Go编程语言规范》中的"select"语句用于选择一组可能的发送或接收操作中的一个进行执行。如果有一个或多个通信操作可以执行,那么会通过均匀的伪随机选择来选择一个可以执行的操作。

你的问题不够明确:如何创建一个最小、完整和可验证的示例。例如,

chan.go:

package main

import (
	"fmt"
	"time"
)

func main() {
    fmt.Println()
	for i := 0; i < 8; i++ {
		one, two := make(chan int), make(chan int)
		go func() { // goroutine one
			select {
			case <-one:
				fmt.Println("read from one")
			case <-two:
				fmt.Println("read from two")
			}
			select {
			case <-one:
				fmt.Println("read from one")
			case <-two:
				fmt.Println("read from two")
			}
            fmt.Println()
		}()
		go func() { // goroutine two
			one <- 1
		}()
		go func() { // goroutine three
			two <- 2
		}()
		time.Sleep(time.Millisecond)
	}
}

输出结果:

$ go run chan.go

read from two
read from one

read from one
read from two

read from one
read from two

read from two
read from one

read from one
read from two

read from two
read from one

read from one
read from two

read from two
read from one

$

你期望的行为是什么,为什么?


《Go编程语言规范》中的"select"语句用于选择一组可能的发送或接收操作中的一个进行执行。通道提供了并发执行函数之间通过发送和接收指定类型的值进行通信的机制。

可以使用内置函数make创建一个新的、初始化的通道值,该函数接受通道类型和可选的容量作为参数:

make(chan int, 100)

容量是指通道缓冲区中元素的数量,它设置了通道缓冲区的大小。如果容量为零或不存在,则通道是无缓冲的,只有在发送方和接收方都准备好时通信才会成功。否则,通道是有缓冲的,如果缓冲区不满(发送)或不空(接收),通信将成功进行而不会阻塞。空通道永远不会准备好进行通信。

"go"语句用于在相同的地址空间内启动一个独立的并发线程或goroutine来执行函数调用。函数值和参数在调用的goroutine中按照通常的方式进行评估,但与常规调用不同,程序执行不会等待被调用的函数完成。相反,该函数在一个新的goroutine中独立地开始执行。当函数终止时,它的goroutine也终止。如果函数有任何返回值,在函数完成时它们将被丢弃。

分析你的新示例:

通道是无缓冲的。goroutine two和three在goroutine one上等待。在无缓冲通道上发送操作会等待,直到有一个待处理的接收操作。当评估goroutine one中的select语句时,通道one或通道two上将有一个待处理的接收操作。现在可以发送并终止在该通道上发送的goroutine two或three。goroutine one现在可以执行该通道上的接收操作并终止。作为一个简单的goroutine同步机制,我们等待goroutine main一毫秒,然后终止它,从而终止任何其他的goroutine。它将终止没有发送的goroutine two或three,因为它们仍在等待待处理的接收操作。

你问道:"读取和写入的次数是否可能不匹配(即one_net和two_net是否可能非零)?例如,在select语句等待从两个通道读取的情况下,然后goroutine two和three执行各自的写入操作,但select只选择其中一个写入操作。"

只有goroutine two和three中的一个可以发送(写入)。将会有一个(发送)写入和一个(接收)读取。这假设在此发生之前,goroutine main不会终止,也就是在一毫秒内发生。

英文:

> The Go Programming Language Specification
>
> Select statements
>
> A "select" statement chooses which of a set of possible send or
> receive operations will proceed.
>
> If one or more of the communications can proceed, a single one that
> can proceed is chosen via a uniform pseudo-random selection.

Your question is imprecise: How to create a Minimal, Complete, and Verifiable example. For example,

chan.go:

package main

import (
	&quot;fmt&quot;
	&quot;time&quot;
)

func main() {
    fmt.Println()
	for i := 0; i &lt; 8; i++ {
		one, two := make(chan int), make(chan int)
		go func() { // goroutine one
			select {
			case &lt;-one:
				fmt.Println(&quot;read from one&quot;)
			case &lt;-two:
				fmt.Println(&quot;read from two&quot;)
			}
			select {
			case &lt;-one:
				fmt.Println(&quot;read from one&quot;)
			case &lt;-two:
				fmt.Println(&quot;read from two&quot;)
			}
            fmt.Println()
		}()
		go func() { // goroutine two
			one &lt;- 1
		}()
		go func() { // goroutine three
			two &lt;- 2
		}()
		time.Sleep(time.Millisecond)
	}
}

Output:

$ go run chan.go

read from two
read from one

read from one
read from two

read from one
read from two

read from two
read from one

read from one
read from two

read from two
read from one

read from one
read from two

read from two
read from one

$

What behavior do you expect and why?


> The Go Programming Language Specification
>
> Channel types
>
> A channel provides a mechanism for concurrently executing functions to
> communicate by sending and receiving values of a specified element
> type.
>
> A new, initialized channel value can be made using the built-in
> function make, which takes the channel type and an optional capacity
> as arguments:
>
> make(chan int, 100)
>
> The capacity, in number of elements, sets the size of the buffer in
> the channel. If the capacity is zero or absent, the channel is
> unbuffered and communication succeeds only when both a sender and
> receiver are ready. Otherwise, the channel is buffered and
> communication succeeds without blocking if the buffer is not full
> (sends) or not empty (receives). A nil channel is never ready for
> communication.
>
> Go statements
>
> A "go" statement starts the execution of a function call as an
> independent concurrent thread of control, or goroutine, within the
> same address space.
>
> The function value and parameters are evaluated as usual in the
> calling goroutine, but unlike with a regular call, program execution
> does not wait for the invoked function to complete. Instead, the
> function begins executing independently in a new goroutine. When the
> function terminates, its goroutine also terminates. If the function
> has any return values, they are discarded when the function completes.

Analyzing your new example:

The channels are unbuffered. Goroutines two and three wait on goroutine one. A send on an unbuffered channel waits until there is a pending receive. When the goroutine one select is evaluated, there will be a pending receive on either channel one or channel two. The goroutine, two or three, that sends on that channel can now send and terminate. Goroutine one can now execute a receive on that channel and terminate. As a crude goroutine synchronization mechanism, we wait goroutine main for one millisecond and then terminate it, which terminates any other goroutines. It will terminate the goroutine, two or three, that didn't get to send because it's still waiting for a pending receive.

You ask "can there even be a mismatch in the number of reads vs the number of writes (i.e. can one_net and two_net be non 0 at the end)? For example in the case where the select statement is waiting on a read from both channels, and then goroutines two and three go through with their respective writes, but then the select only picks up on one of those writes."

Only one of goroutines two and three gets to send (write). There will be exactly one (send) write and one (receive) read. This assumes that goroutine main does not terminate before this occurs, that is, it occurs within one millisecond.

答案2

得分: 0

正如peterSO指出的那样,选择准备好的多个同时通道是伪随机的。

然而,需要注意的是,在大多数情况下,发送和/或接收的goroutine之间会存在竞争条件,这也引入了不确定性。

事实上,peterSO的示例说明了这种情况;在接收goroutine达到第一个select语句的时候,无法保证任何一个或两个发送goroutine是否已经执行了它们各自的发送语句。以下是相关的代码片段,其中添加了一些注释:

a, b := make(chan int), make(chan int)
go func() { // goroutine one
    // 在这一点上,任何一个或没有一个通道都可能准备好。
    select {
    case <-a:
        fmt.Println("从a读取")
    case <-b:
        fmt.Println("从b读取")
    }
    // 在这一点上,我们将读取一个值,并阻塞等待另一个值。
    select {
    case <-a:
        fmt.Println("从a读取")
    case <-b:
        fmt.Println("从b读取")
    }
    fmt.Println()
}()
go func() { // goroutine two
    a <- 1 // 这个会先执行吗?
}()
go func() { // goroutine three
    b <- 2 // 还是这个会先执行?
}()

一般来说,在编写并发程序时,应避免依赖于并发事件以任何特定确定的顺序发生。除非程序逻辑串行化了这些事件,作为一个经验法则,将它们视为以不确定的(尽管不一定是随机和均匀分布的)顺序发生,这样你就会更安全,而不是遗憾。

英文:

As peterSO points out, selection among multiple simultaneous channels that are ready is pseudo-random.

However, it is important to notice that in most cases, you will have race conditions between the sending and/or receiving goroutines, which also introduces indeterminism.

In fact, peterSO's example illustrates this very situation; at the point where the receiving goroutine reaches the first select statement, there is no guarantee whether any or both of the sending goroutines have executed their respective send statement. The relevant snippet follows, with some added comments:

a, b := make(chan int), make(chan int)
go func() { // goroutine one
    // At this point, any or none of the channels could be ready.
    select {
    case &lt;-a:
        fmt.Println(&quot;read from a&quot;)
    case &lt;-b:
        fmt.Println(&quot;read from b&quot;)
    }
    // At this point, we will have read one, and will block waiting for the other.
    select {
    case &lt;-a:
        fmt.Println(&quot;read from a&quot;)
    case &lt;-b:
        fmt.Println(&quot;read from b&quot;)
    }
    fmt.Println()
}()
go func() { // goroutine two
    a &lt;- 1 // Does this execute first?
}()
go func() { // goroutine three
    b &lt;- 2 // ...or does this?
}()

In general, when writing concurrent programmes, one should avoid relying on concurrent events happening in any particular determined order. Unless your program logic serialises things, as a rule of thumb, consider them happening in an indeterminate (though not necessarily random and evenly distributed) order, and you will be safe more often than sorry.

huangapple
  • 本文由 发表于 2017年6月25日 18:22:51
  • 转载请务必保留本文链接:https://go.coder-hub.com/44745406.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定