Search code examples
gogo-playground

Closing stop channel does not stop goroutines


INTRODUCTION:

I am just starting to learn the Go language and have reached the lesson about concurrency.

I have invented a small task for myself to try to implement what I have learned about closing goroutines.

PROBLEM:

If we close channel it's case will always be selected in the select statement, which is a great way to broadcast cancelation signal to all goroutines.

Below I have 2 goroutines and a quit channel that never gets received.

Code timeouts on Playground.

If I comment out default part in the goroutines then quit signal gets received.

QUESTION:

I really do not understand why is this happening and how to fix it, although I am trying.
Can somebody please explain me what is the problem and offer some advice on how to solve it?

package main

import (
    "fmt"
    "sync"
    "time"
)

func positive_numbers(quit chan struct{}, wg *sync.WaitGroup) {
    defer wg.Done()
    i := 1
    for {
        select {
        case <-quit:
            fmt.Println("[+]Quiting...")
            return
        default:
            fmt.Printf("%v ", i)
            i++
        }
    }
}

func negative_numbers(quit chan struct{}, wg *sync.WaitGroup) {
    defer wg.Done()
    i := -1
    for {
        select {
        case <-quit:
            fmt.Println("[-]Quiting...")
            return
        default:
            fmt.Printf("%v ", i)
            i--
        }
    }
}

func main() {
    quit := make(chan struct{})

    wg := sync.WaitGroup{} // so we can wait for all goroutines to finish
    wg.Add(2)

    go positive_numbers(quit, &wg)
    go negative_numbers(quit, &wg)

    go func(quit chan struct{}) {
        defer close(quit)
        time.Sleep(1 * time.Second)
    }(quit)

    wg.Wait()
}

Solution

  • This code works alright in real life; it's just not compatible with the playground's "fake time" because positive_numbers and negative_numbers don't block, and the runtime can't decide how many iterations of each get to run before the Sleep expires (basically, the prints take zero emulated wallclock time, so they try to produce infinite output, and you hit the playground CPU usage limit without ever advancing the wallclock time at all). On real machines, where you can only print finite output in finite time, you get reasonable behavior.

    In the playground, if you add something like time.Sleep(time.Millisecond) after each print, you force the fake clock to move forward, and the program also terminates.