Channel non-determinism using context timeouts, deadlocks

Issue

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
        }
    }
}

Answered By – Andrey Dyatlov

Answer Checked By – Terry (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.