Sunday, May 1, 2016

GoRoutines: Are They a Tree, or Independent?

I was working on a side project when I ran into a question regarding goroutines spawning goroutines; if you have spawn a goroutine from main() (I'll call it Offspring1), and Offspring1 spawns a goroutines called Offspring2, then Offspring1 returns(), what happens to Offspring2?

Does it die, like pruning a branch off a process tree?

Or does Offspring2 keep running?

I wrote a small test application to find out.

The Test:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
 "fmt"
 "time"
)

var chanRunner2 = make(chan string)
var chanRunner1 = make(chan string)
var chanStop1 = make(chan bool)

func main() {

 a := time.NewTimer(5 * time.Second)
 b := time.NewTimer(10 * time.Second)

 go Runner1()

 for {
  select {
  case <-a.C:
   chanStop1 <- true
  case strMessage := <-chanRunner1:
   fmt.Println(strMessage)
  case strMessage := <-chanRunner2:
   fmt.Println(strMessage)
  case <-b.C:
   fmt.Println("DONE!")
   return
  default:
   continue
  }
 }
}

func Runner1() {

 go Runner2()

 c := time.NewTicker(500 * time.Millisecond)

 for {
  <-c.C
  select {
  case <-chanStop1:
   return
  default:
   chanRunner1 <- "Howdy from Runner1!"
  }
 }
}

func Runner2() {

 d := time.NewTicker(500 * time.Millisecond)

 for {
  <-d.C
  chanRunner2 <- "Hello from Runner2!"
 }
}

Like my previous "let's test a theory" test applications, this one is pretty straightforward. There are two functions; Runner2(), whose only job is to create a ticker that ticks every 500 milliseconds and when that tick fires it sends "Hello from Runner2!" to a channel called chanRunner2.

Runner1() is just like Runner2(), except it first spawns Runner2() before it starts firing a slightly different message into a channel called chanRunner1 every 500 milliseconds. There is one other small addition; Runner1() listens to a channel called chanStop1 and if anything comes down the pipeline, it calls return.

Then there's main(); main() creates two timers (not tickers), one that will fire in 5 seconds and one that will fire in 10 seconds. Main() then spawns Runner1() and starts a loop listening for either a timer to fire or a message from channels chanRunner1 or chanRunner2, with a default of "continue" so the select statement keeps re-evaluating in a loop.

Expected Output:

Because of the nature of goroutines and the tickers (not timers...there's an important difference...) the output should be "Howdy from Runner1!" interspersed with "Hello from Runner2!". After 5 seconds, the first timer fires, and Runner1() calls return; either both lines stop writing to the console because Runner1() returns and kills Runner2() with it, or "Hello from Runner2!" continues for the next 5 seconds without the other message interleaved, meaning that you can kill the routine that created another goroutine without having any effect on the "grandchild" goroutine to main().

Actual Output:

Drumroll, please...

./chained_goroutines
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Howdy from Runner1!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
Hello from Runner2!
DONE!

There it is; Runner2() kept running after Runner1() exited. Something to keep in mind when modeling how your application works!