Client request handler example from Effective Go leads to deadlock?

Issue

The Effective Go guide has the following example on handling client requests:

func handle(queue chan *Request) {
    for r := range queue {
        process(r)
    }
}

func Serve(clientRequests chan *Request, quit chan bool) {
    // Start handlers
    for i := 0; i < MaxOutstanding; i++ {
        go handle(clientRequests)
    }
    <-quit  // Wait to be told to exit.
}

I ran similar code locally where client requests were just integers:

func handle(queue chan int) {
    for r := range queue {
        fmt.Println("r = ", r)
    }
}

func serve(clientRequests chan int, quit chan bool) {
    // Start handlers
    for i := 0; i < 10; i++ {
        go handle(clientRequests)
    }
    <-quit // Wait to be told to exit.
}

var serveChannel = make(chan int)
var quit = make(chan bool)
serve(serveChannel, quit)
for i := 0; i < 10; i++ {
    serveChannel <- i
}

However my code results in deadlock error fatal error: all goroutines are asleep - deadlock!.

Even though I don’t understand the problem in my program conceptually I also don’t understand how the original code works. I do understand that MaxOutstanding goroutines are spawned and they all listen to the single clientRequests channel. But clientRequests channel is only for one request so once a request comes in then all goroutines have the access to the same request. Why is this useful?

Solution

The code that calls serve is not supposed to run in the same goroutine as the one that fills up the channel.

In your code, serve launches the handler goroutines but then waits for <-quit. Since it’s blocked, you never reach the code that populates serveChannel. So the workers never have anything to consume. You also never notify quit, leaving serve to wait forever.

The first step is to send data to serveChannel in a separate goroutine. eg:

func handle(queue chan int) {
    for r := range queue {
        fmt.Println("r = ", r)
    }
}

func serve(clientRequests chan int, quit chan bool) {
    // Start handlers
    for i := 0; i < 10; i++ {
        go handle(clientRequests)
    }
    <-quit // Wait to be told to exit.
}

func populateRequests(serveChannel chan int) {
    for i := 0; i < 10; i++ {
        serveChannel <- i
    }
}

func main() {
    var serveChannel = make(chan int)
    var quit = make(chan bool)
    go populateRequests(serveChannel)
    serve(serveChannel, quit)
}

We now have all requests being handled as desired.

However, you’ll still encounter all goroutines are asleep once processing is done. This is because serve ends up waiting for the quit signal but there’s nothing to send it.

In a normal program, quit would be populated after catching a signal or some shutdown request. Since we don’t have anything, we’ll just have it shut down after three seconds, also in a separate goroutine.

func handle(queue chan int) {
    for r := range queue {
        fmt.Println("r = ", r)
    }
}

func serve(clientRequests chan int, quit chan bool) {
    // Start handlers
    for i := 0; i < 10; i++ {
        go handle(clientRequests)
    }
    <-quit // Wait to be told to exit.
}

func populateRequests(serveChannel chan int) {
    for i := 0; i < 10; i++ {
        serveChannel <- i
    }
}

func quitAfter(quit chan bool, duration time.Duration) {
    time.Sleep(duration)
    quit <- true
}

func main() {
    var serveChannel = make(chan int)
    var quit = make(chan bool)
    go populateRequests(serveChannel)
    go quitAfter(quit, 3*time.Second)
    serve(serveChannel, quit)
}

As for your last question: the multiple handlers won’t be seeing the same requests. The moment one handler receives a value from the channel, that value is removed from it. The next handler will receive the next value. Think of a channel as a First-In-First-Out queue that is safe for concurrent use.

You can find the last iteration of the code on the playground.

Answered By – Marc

Answer Checked By – Clifford M. (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.