Search code examples
gochannelgoroutine

Making sense of golang blocking channel behaviour with multiple writes to the channel


I am new in golang and trying to understand the concurrency in the language. I have a code which pushes a few values to the channel and then reads them.

package main

import (
    "log"
    "time"
)

func Greet2(c chan string) {
    // logging to Stdout is not an atomic operation
    // so artificially, sleep for some time
    time.Sleep(2 * time.Second)
    
    // 5. until below line reads and unblock the channel
    log.Printf("5. Read Greet2:: %s\n\n", <-c)
}

func Greet(c chan string) {
    // 4. Push a new value to the channel, this will block
    // Process will look for other go routines to execute
    log.Printf("4. Add 'Greet::John' to the channel, block until it is read. Remember, 'Greet' goroutine will block and only other goroutines can run even though this go routine can pull the value out from the channel.\n\n")
    c <- "Greet::John!"
    
    // 8. This statement will never execute
    log.Printf("8. Read Greet:: %s !\n\n", <-c)
}

func main() {

    c := make(chan string)

    log.Println("1. Main start")
    
    // 2. Both go routine will be declared and both will
    // for a value to be inserted in the channel
    log.Println("2. Declare go routines.\n\n")
    go Greet(c)
    go Greet2(c)
    
    // 3. write will block
    log.Println("3. Add 'main::Hello' to the channel, block until it is read. Remember, 'main' goroutine will block and only other goroutines can run even though this go routine can pull the value out from the channel.\n\n")
    c <- "main::Hello"
    
    // Sleep to give time goroutines to execute
    time.Sleep(time.Second)
    
    // 6. read the channel value.
    log.Printf("6. Read main:: %s \n\n", <-c)
    
    // 7. Insert a new value to the channel
    log.Println("7. Add 'main::Bye' to the channel, block until it is read.\n")
    c <- "main::Bye"
    
    // Sleep to give time goroutines to execute
    time.Sleep(time.Second)
    log.Println("9. Main stop")

}

the output of the above program is

2023/09/02 21:58:07 1. Main start
2023/09/02 21:58:07 2. Declare go routines.


2023/09/02 21:58:07 3. Add 'main::Hello' to the channel, block until it is read. Remember, 'main' goroutine will block and only other goroutines can run even though this go routine can pull the value out from the channel.


2023/09/02 21:58:07 4. Add 'Greet::John' to the channel, block until it is read. Remember, 'Greet' goroutine will block and only other goroutines can run even though this go routine can pull the value out from the channel.

2023/09/02 21:58:10 5. Read Greet2:: main::Hello

2023/09/02 21:58:11 6. Read main:: Greet::John!

2023/09/02 21:58:11 7. Add 'main::Bye' to the channel, block until it is read.

2023/09/02 21:58:11 8. Read Greet:: main::Bye !

2023/09/02 21:58:12 9. Main stop

I am unable to understand why 4.(another write to the channel) is getting executed before 5.(First read from the channel) as 3. will block and channel would not be available until the value is read from it(in step 5.). Am I misunderstanding the blocking behaviour, In step 3. only main goroutine blocks and Greet (in step 4.) can write additional value to the channel? An explanation would really resolve my confusion :)

Cheers, DD.


May thanks to the replies and I have created a simpler program to demo. the concurrency

package main

import (
        "fmt"
)

func do2(c chan int) {
        fmt.Println(<-c)
}

func do(c chan int) {
        // 4. this statement is trying to write another value "2" to the channel
        // Channel already contains "1" as the value which has not been read yet.
        // this statement will wait for "1" to get read and block the execution.
        // Scheduler will look for other goroutines that can execute.
        // However, this("do") is blocked as well as "main" is blocked too and
        // there are no other goroutines to execute.
        // Hence, will result in a "Deadlock" fatal error.
        c <- 2
        fmt.Println(<-c)
}

func main() {

        // 1. Declare a channel
        c := make(chan int)
        // 2. Declare "do" goroutine
        go do(c)
        // 3. write "1" to the channel
        // This will block and wait for program's other goroutines to read the value.
        // however, there is only "do" goroutine is defined can run at this point.
        // Scheduler, will try to run "do" goroutine.
        c <- 1
        go do2(c)

}

the Deadlock can be fixed by swapping c <- 1 and go do2(c) statements.


Solution

  • In Go, when you send a value on a channel (c <- "main::Hello" in step 3), the sending goroutine will block until there is another goroutine ready to receive the value from the channel. However, this doesn't mean that no other goroutines can proceed. In your code, both Greet and Greet2 goroutines are waiting for values from the channel, so when you send the value in step 3, one of them (it's not guaranteed which one) will unblock and proceed to execute.

    Let me break down the sequence of events step by step:

    1. Main starts, and you create a channel c.
    2. You declare two goroutines, Greet and Greet2, and both are waiting for values from the channel.
    3. You send a value "main::Hello" on the channel, and this blocks the main goroutine until some other goroutine reads from the channel. However, one of the two goroutines (Greet or Greet2) is unblocked to receive this value.
    4. Greet unblocks and proceeds to execute. It logs the message "4. Add 'Greet::John' to the channel..." and sends "Greet::John!" on the channel. This blocks Greet again since there's no other goroutine to read from the channel at that moment.
    5. Greet2 unblocks and proceeds to execute. It logs the message "5. Read Greet2:: main::Hello" and reads the value "main::Hello" from the channel.
    6. Main unblocks, logs "6. Read main:: Greet::John!" and reads "Greet::John!" from the channel.
    7. Main sends another value "main::Bye" on the channel. At this point, Greet is still blocked on writing to the channel, and Greet2 is blocked because it's not reading from the channel.
    8. Since Greet is still blocked on writing, it never gets to log "8. Read Greet:: main::Bye !"
    9. Main stops.

    So, the key to understanding the behavior here is that when you send a value on a channel, it unblocks any goroutine that is waiting to read from the channel. The order in which the waiting goroutines get unblocked is not determined and depends on the scheduler. In your case, Greet2 happened to get unblocked first, but it could have been Greet as well.

    In summary, the behavior you are observing is entirely consistent with how Go's channels work, and it's important to note that the order of execution between competing goroutines is not guaranteed.