Deadlock Happens Due To Ranging Over Open Channel But Panic Not Thrown

79 views Asked by At

I have this code that I deliberately wrote to cause a deadlock and a subsequent panic throw:

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Start:", time.Now())
    channel1 := make(chan string, 2)

    channel1 <- "Cat"
    channel1 <- "Dog"

    limiter := time.NewTicker(time.Millisecond * 500)
    for channel1Item := range channel1 {
        <-limiter.C
        fmt.Println(channel1Item)
    }

    fmt.Println("End:", time.Now())
}

In a way, a deadlock does happen and the code just hangs indefinitely, however, it does not throw a panic. If I remove the limiter (ticker), it then does. Why does the ticker prevent the panic from being thrown?

I am currently learning Go and I accidentally stumbled into this example when changing random bits to satiate my curiosity. The strange behavior (of a panic not being throw) happened after I decided not to close the channel to see what would happen.

I know ranging over an open channel that is not being fed generally throws a panic, but it seems that having a ticker inside the loop interrupts the panic throwing behavior.

2

There are 2 answers

1
ed_Chris On

Because the judgment in GO is that when a goroutine attempts to send a worthy message on a closed channel, it will trigger panic.

In your code, you did not close channel1, so the loop will wait indefinitely for channel1 to accept the value. <- limiter. C will periodically send values to the channel So channel1 won't rule out panic, loops with limiters will hang Even if he doesn't have any new value

I hope it can be helpful to you

0
Brits On

I know ranging over an open channel that is not being fed generally throws a panic

I guess that this is correct, a deadlock will be detected (and a panic triggered) if "ranging over an open channel" is the only thing that is happening (or all other goroutines are also blocked). A deadlock will be detected in this (playground):

func main() {
    c := make(chan int)
    for _ = range c {
    }
}

but not in this (playground):

func main() {
    go func() {
        for {
            time.Sleep(time.Nanosecond)
        }
    }()

    c := make(chan int)
    for _ = range c {
    }
}

That's because, while for _ = range c { is blocked, the goroutine is not (so there is no deadlock).

The code that identifies deadlocks is relatively simple and triggers a panic if there is nothing that can run. It does not check that the code that is running is doing something useful!

As per the docs for Ticker:

The ticker will adjust the time interval or drop ticks to make up for slow receivers.

The ticker will continue to run whether or not something is listening on the channel; its similar to adding something like the following to your code (playground):

channel2 := make(chan time.Time, 1)
go func() {
    for {
        time.Sleep(time.Millisecond * 500)
        select {
        case channel2 <- time.Now():
        default:
        }
    }
}()

This loop will continue to run regardless of whether something is listening on channel2 or not. As such, no deadlock will be detected (and no panic triggered). Adding the Ticker has the same impact; the fact that the Ticker is running means that no deadlock will be detected.