Is blocking on a channel send a bad synchronization paradigm and why

Issue

Effective Go gives this example on how to emulate a semaphore with channels:

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    <-sem
    process(r)
    sem <- 1
}

func init() {
    for i := 0; i < MaxOutstanding; i++ {
        sem <- 1
    }
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)
    }
}

It also says: Because data synchronization occurs on a receive from a channel (that is, the send “happens before” the receive; see The Go Memory Model), acquisition of the semaphore must be on a channel receive, not a send.

Now, I think I understand the Go Memory Model and the definition of “happens before.” But I fail to see what’s the problem with blocking on a channel send:

func handle(r *Request) {
    sem <- 1
    process(r)
    <-sem
}

func init() {}

This code (with sem and Serve unchanged from above) uses the buffered channel in the opposite way. The channel starts empty. On entering handle, the send will block if there are already MaxOutstanding goroutines doing the process. As soon as one of them finishes its processing and “frees” a slot from the channel, by receiving one int, our send will be unblocked and the goroutine will start its own processing.

Why is this a bad way to do synchronization, as the textbook seems to imply?

Does a receive operation that frees a channel slot not “happen before” the send that will use that same slot? How is this possible?


In other words, the Language Reference says that “a send on a buffered channel [blocks until] there is room in the buffer.”

But the Memory Model only says that “A receive from an unbuffered channel happens before the send on that channel completes.” In particular, it does not say that a receive from a buffered channel that is full happens before a send on that channel completes.

Is this some corner case that can not be trusted to do the Right Thing? (which would be actually synchronizing a send that was blocked with the receive that unblocks it)

If that’s the case, it looks like a nasty race condition in a language designed to minimize sneaky race conditions 🙁

var c = make(chan int, 1)
var a string

func f() {
    a = "hello, world"
    <-c  // unblock main, which will hopefully see the updated 'a'
}

func main() {
    c <- 0  // fill up the buffered channel
    go f()
    c <- 0  // this blocks because the channel is full
    print(a)
}

Solution

This bit of the Effective Go document threw me also. In fact, in relatively recent versions of Effective Go, the code in question acquired the semaphore on a channel send (instead of a channel receive like it does in the current version, which uses the init() to “prime” the channel).

There has apparently been a good deal of discussion on the topic. I won’t bother trying to summarize everything, but the discussion can all be found from here:

https://code.google.com/p/go/issues/detail?id=5023

It does strike me as unfortunate, but quoting the filer of that issue, the short story appears to be that unless the semaphore is acquired on the channel receive…:

The following code:

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    process(r)  // May take a long time.
    <-sem       // Done; enable next request to run.
}

…could legally be “optimized” into:

func handle(r *Request) {
    process(r)  // May take a long time.
    sem <- 1    // Wait for active queue to drain.
    <-sem       // Done; enable next request to run.
}

…or into:

func handle(r *Request) {
    sem <- 1    // Wait for active queue to drain.
    <-sem       // Done; enable next request to run.
    process(r)  // May take a long time.
}

Answered By – bgmerrell

Answer Checked By – Katrina (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.