Search code examples
godeadlockchannelgoroutine

Channel non-determinism using context timeouts, deadlocks


I'm trying to understand contexts and channels in Go, but I'm having trouble wrapping my head around what's happening. Here's some example code.

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "golang.org/x/time/rate"
)

func main() {
    msgs := make(chan string)

    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    limiter := rate.NewLimiter(rate.Every(2*time.Second), 1)

    go func(ctx context.Context, limiter *rate.Limiter) {
        for {
            limiter.Wait(context.Background())

            select {
            case <-ctx.Done():
                log.Printf("finished!")
                return
            case msg := <-msgs:
                log.Printf("receiving a message: %s", msg)
            }
        }
    }(ctx, limiter)

    defer close(msgs)

    i := 0
    for {
        msgs <- fmt.Sprintf("sending message %d", i)
        i++
        if i > 10 {
            break
        }
    }
}

The results I'm getting are non-deterministic. Sometimes the logger prints out three messages, sometimes it's five. Also, the program ends in a deadlock every time:

2021/12/31 02:07:21 receiving a message: sending message 0
2021/12/31 02:07:23 receiving a message: sending message 1
2021/12/31 02:07:25 receiving a message: sending message 2
2021/12/31 02:07:27 receiving a message: sending message 3
2021/12/31 02:07:29 receiving a message: sending message 4
2021/12/31 02:07:29 finished!
fatal error: all goroutines are asleep - deadlock!

So, I guess I have a couple of questions:

  • Why doesn't my goroutine simply end after one second?
  • Why is there a deadlock? How can I avoid deadlocks of this nature?

Solution

  • Why doesn't my goroutine simply end after one second?

    While the goroutine may wait here instead of the select:

    limiter.Wait(context.Background())
    

    Why is there a deadlock? How can I avoid deadlocks of this nature?

    It is your main goroutine which is getting stuck. It happens here:

    msgs <- fmt.Sprintf("sending message %d", I)
    

    There are no goroutines that would read from msgs, so it waits forever.

    Here is one of the ways to make it work:

    package main
    
    import (
        "context"
        "fmt"
        "log"
        "time"
    
        "golang.org/x/time/rate"
    )
    
    func main() {
        msgs := make(chan string)
    
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()
    
        limiter := rate.NewLimiter(rate.Every(1*time.Second), 1)
    
        go func() {
            for {
                limiter.Wait(context.Background())
    
                select {
                case <-ctx.Done():
                    log.Printf("finished!")
                    return
                case msg := <-msgs:
                    log.Printf("receiving a message: %s", msg)
                }
            }
        }()
    
        defer close(msgs)
    
        for i := 0; i < 100000; i++ {
            select {
            case msgs <- fmt.Sprintf("sending message %d", i):
            case <-ctx.Done():
                log.Printf("finished too!")
                return
            }
        }
    }