试图了解goroutines

I've been playing around with the following code from A Tour of Go, but I don't understand what is going on when I apply some minor changes. The original code is this

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

and it produces this

world
hello
hello
world
world
hello
hello
world
world
hello

which is OK: five times hello, five times world. I starts to get strange when I call

say("world")
go say("hello")

Now the output is just

world
world
world
world
world

No hello whatsoever. It gets even weirder with two goroutines

go say("world")
go say("hello")

Now there is no output at all. When I change i < 5 to i < 2 and call

go say("world")
say("hello")

I get

world
hello
hello

What am I missing here?

In the case of

 say("world")
 go say("hello")

The "world" call must complete before the "hello" goroutine is started. The "hello" goroutine does not run or complete because main returns.

For

go say("world")
go say("hello")

the goroutines do not run or complete because main returns.

Use sync.WaitGroup to prevent main from exiting before the goroutines complete:

func say(wg *sync.WaitGroup, s string) {
  defer wg.Done()
  for i := 0; i < 5; i++ {
    time.Sleep(100 * time.Millisecond)
    fmt.Println(s)
  }
}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go say(&wg, "world")
  go say(&wg, "hello")
  wg.Wait()
}

playground example

It is because the main function has been exited.

When main function return, all goroutines are abruptly terminated, then program exits.

You add a statment:

time.Sleep(100 * time.Second)

before main function return, and everything goes well.

But a good practice in Go is to use channel, which is used to communicate between goroutines. You can use it to let main function wait for background goroutines to finish.

With

func main() { go say("world") say("hello") }

You are creating two separate goroutines, one is the main functions goroutine and one is the go say("world"). Normally when functions are executed the programs jumps to that function, execute all code inside and then jumps to the line after where the function was called from.

With goroutines you are not jumping inside the function but you are starting the goroutine in a separate thread and continuing to execute the line just after the call without waiting for it.

Therefore the goroutine will not have time to finish before the main goroutine is done.

Congratulations for learning Go. As someone new, it is nice to understand concurrency and how it is different from parallelism.

Concurrency
Concurrency is like a juggler juggling several balls in the air with one hand. No matter how many balls he is juggling, only one ball touch his hand at any moment.

Parallelism
When the juggler starts juggling more balls with another hand in parallel, we have two concurrent processes running at the same time.

Goroutines are great because they're both concurrent and auto-parallel, depending on the computing cores available and the GOMAXPROCS variable being set.

The One-handed Juggler
Back to the one-handed, single-cored, concurrent juggler. Imagine him juggling three balls named "hello", "world", and "mars" respectively with the hand being the main routine.

var balls = []string{"hello", "world", "mars"}

func main() {
        go say(balls[0])
        go say(balls[1])
        go say(balls[2])
}

Or more appropriately,

func main() {
        for _, ball := range balls {
                go say(ball)
        }
}

Once the three balls are thrown up into the air sequentially, the juggler simply retreats his hand right away. That is, the main routine exits before the first ball thrown can even land on his hand. Shame, the balls just drop to the ground. Bad show.

In order to get the balls back in his hand, the juggler has to make sure he waits for them. This means his hand needs to be able to keep track of and count the balls he threw and learn when each is landing.

The most straightforward way is to use sync.WaitGroup:

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

var balls = []string{"hello", "world", "mars"}
var wg sync.WaitGroup

func main() {
        for _, ball := range balls {
                // One ball thrown
                wg.Add(1)
                go func(b string) {
                        // Signals the group that this routine is done.
                        defer wg.Done()
                        // each ball hovers for 1 second
                        time.Sleep(time.Duration(1) * time.Second)
                        fmt.Println(b)
                        // wg.Done() is called before goroutine exits
                }(ball)
        }

        // The juggler can do whatever he pleases while the 
        // balls are hanging in the air.

        // This hand will come back to grab the balls after 1s.
        wg.Wait()
}

WaitGroup is simple. When a goroutine is spawned, one adds to a "backlog counter" with WaitGroup.Add(1) and call WaitGroup.Done() to decrease the counter. Once the backlog becomes 0, it means that all goroutines are done and WaitGroup should stop waiting (and grab the balls!).

While using channel(s) for synchronization is fine, it is encouraged to use available concurrent tools as appropriate especially when the use of channels make the code more complex and hard to comprehend.