Search code examples
concurrencygo

understand the code - Go concurrency pattern: Daisy Chain


I was studying Go concurrency pattern.

One pattern I am not sure is: Daisy Chain https://talks.golang.org/2012/concurrency.slide#39

It's very hard for me to understand the control flow of the code.

Can someone explain to me ?

package main

import (
    "fmt"
)

func f(left, right chan int) {
    left <- 1 + <-right
}

func main() {
    const n = 10000

    leftmost := make(chan int)
    right := leftmost               //point B: what does these do ?
    left := leftmost

    for i := 0; i < n; i++ {
        right = make(chan int)
        go f(left, right)
        left = right                //point A
    }
    go func(c chan int) { c <- 1 }(right)  
    fmt.Println(<-leftmost)
}

Conclusion:

  1. the flow of channel going from right to left. It is good practice to write func f(left chan<- int, right <-chan int) rather than original function signature as above.

  2. 'chain reaction' does not start until c <- 1, when signal 1 is sent to right most channel, reaction goes all the way to left most end. Print out 10001.

The reason is go channel block 'read' until received channel receive signal.

  1. @Rick-777 shows how to use array like structure for easy understanding. Since each go coroutine is just around 6k big. It's not a bad idea to make 10k channel.

  2. I clean up some code around Point B, for channel initialization. Here is the source code: http://play.golang.org/p/1kFYPypr0l


Solution

  • I found dry-run this program could be really helpful to understand it.

    At first, after executing

    leftmost := make(chan int)
    right := leftmost
    left := leftmost
    

    leftmost, left, and right are all referring to the same chan int

    [chan int]
         |                 
    left, leftmost, right
    

    Let's run some iterations for the for-loop.

    i = 0

    When we just enter the for loop,

    [chan int]
         |                 
    left, leftmost, right
    

    after executing right = make(chan int) and go f(left, right).

    [chan int] <-(+1)- [chan int]
         |                 |
    left, leftmost        right
    

    after executing left = right

    [chan int] <-(+1)- [chan int]
         |                 |
      leftmost        left, right
    

    i = 1

    When we just enter the for loop,

    [chan int] <-(+1)- [chan int]
         |                 |
      leftmost        left, right
    

    after executing right = make(chan int) and go f(left, right).

    [chan int] <-(+1)- [chan int] <-(+1)- [chan int]
         |                 |                   |
      leftmost            left               right
    

    after executing left = right

    [chan int] <-(+1)- [chan int] <-(+1)- [chan int]
         |                                     |
      leftmost                            left, right
    

    I feel like two loops are enough to see the pattern:

    • Every loop we create a new chan int and append it at the end of the "linked list of chan int".
    • So after n = 100000 loops, we created 100000 new chan int, and the number of chan int in the "linked list of chan int will be 100001.
    • 100001 chan int means 100000 gaps between each pair of adjacent chan int, and each gap means one +1.

    Before the for loop, because all chan int are acting as receivers and there is no pass-in value, so all chan int will just wait.

    After the for loop, we execute go func(c chan int) { c <- 1 }(right), then the 1 is passed into the "linked list of chan int" and perform +1 on the value for 100000 times, so the final result to the leftmost will be 100001.

    Things will be like when we pass 1 into the "linked list of chan int":

    [chan int] <-(+1)- [chan int] <-(+1)- ...... <-(+1)- [chan int] <- 1
         |                                                   |
      leftmost                                           left, right
    

    I created a leetcode playground holding all the code. You could try it here (https://leetcode.com/playground/gAa59fh3).