Curious
Curious

Reputation: 21510

Expected behavior when multiple things happen together in select

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

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

and one one goroutine is waiting on the following send

one <- 1

and another is waiting on the following

two <- 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 (
    "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()
}

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.

Upvotes: 1

Views: 328

Answers (2)

Josef Grahn
Josef Grahn

Reputation: 1635

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 <-a:
        fmt.Println("read from a")
    case <-b:
        fmt.Println("read from b")
    }
    // At this point, we will have read one, and will block waiting for the other.
    select {
    case <-a:
        fmt.Println("read from a")
    case <-b:
        fmt.Println("read from b")
    }
    fmt.Println()
}()
go func() { // goroutine two
    a <- 1 // Does this execute first?
}()
go func() { // goroutine three
    b <- 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.

Upvotes: 0

peterSO
peterSO

Reputation: 166569

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 (
    "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)
    }
}

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.

Upvotes: 2

Related Questions