Home > Software engineering >  Channel non-determinism using context timeouts, deadlocks
Channel non-determinism using context timeouts, deadlocks

Time:01-01

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?

CodePudding user response:

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

CodePudding user response:

Sending or receiving a message using <- is a blocking operation. When the context timeout finishes, the goroutine has now exited and the caller can't proceed.

// Goroutine has finished, this call will never finish
msgs <- fmt.Sprintf("sending message %d", i)

At this point the program has resulted in a deadlock.

As for the non-determinism, the goroutine and the main execution context run concurrently. Because there are two for loops without much delay, the threads are competing with each other. There is no guarantee that they will execute in the same fashion every time.

  • Related