Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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)
}
like image 985
Tobia Avatar asked May 01 '13 19:05

Tobia


People also ask

What is a channel synchronization?

Synchronization Channel is a downlink only control channel used in GSM cellular telephone systems. It is part of the Um air interface specification. The purpose of the SCH is to allow the mobile station (handset) to quickly identify a nearby cell (a BTS) and synchronize to that BTS's TDMA structures.

Are buffered channels blocking?

Buffered channel are blocked only when the buffer is full. Similarly receiving from a buffered channel are blocked only when the buffer will be empty. Buffered channels can be created by passing an additional capacity parameter to the make( ) function which specifies the size of the buffer.


1 Answers

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.
}
like image 199
bgmerrell Avatar answered Oct 02 '22 15:10

bgmerrell