解析Go语言阻塞通道的多写入行为
Go语言无缓冲通道并发阻塞疑问
我是Go语言新手,正在学习该语言的并发机制。我编写了如下代码,用于向通道推送值并读取:
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") }
程序运行输出如下:
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
我无法理解为什么步骤4(向通道写入新值)会在步骤5(首次读取通道值)之前执行。我原以为步骤3的写入会阻塞通道,直到步骤5读取值后通道才可用。是不是我误解了阻塞行为?难道只有main goroutine在步骤3阻塞,而Greet goroutine仍能执行步骤4的写入操作?希望得到解释以消除我的困惑。
感谢各位的回复,我还编写了一个更简单的并发演示程序:
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) }
该程序会触发Deadlock(死锁)错误,通过交换c <- 1和go do2(c)语句可修复死锁。
核心解释
无缓冲通道的阻塞本质
你混淆了"通道阻塞"和"goroutine阻塞"的概念:无缓冲通道的写入操作只会阻塞执行写入的那个goroutine,而非锁住整个通道不让其他goroutine操作。
针对第一个示例的流程拆解:
- 你创建的是无缓冲通道
c := make(chan string),这类通道的核心规则是:写入必须等待读取,读取必须等待写入,但这个等待是针对单个goroutine的,不是通道全局锁。 - main goroutine执行
c <- "main::Hello"(步骤3)时,main进入阻塞状态,但此时Greet goroutine已经被Go调度器启动并执行——它会先打印步骤4的日志,然后尝试执行c <- "Greet::John!",此时Greet goroutine也进入阻塞状态(因为还没有goroutine读取通道)。 - 2秒后Greet2 goroutine的sleep结束,执行
<-c读取值:这一步会同时完成两个动作——main goroutine的写入操作完成(解除阻塞),Greet2拿到值并打印步骤5的日志。 - 后续main goroutine继续执行sleep,然后步骤6读取通道值,解除Greet goroutine的阻塞;Greet goroutine接下来执行
<-c又会阻塞,直到步骤7main写入"main::Bye",才会完成读取并打印步骤8的日志。
第二个死锁示例的原因
- 原代码中,main执行
c <-1时进入阻塞,等待读取;此时只有do goroutine在运行,但do goroutine执行c <-2时也会阻塞(没有goroutine读取通道)。 - 此时所有goroutine都处于阻塞状态,没有任何goroutine能执行读取操作打破僵局,因此触发死锁。
- 交换
c <-1和go do2(c)后,do2 goroutine提前启动,当main执行c <-1时,do2可以读取值,解除main的阻塞,后续流程正常推进,避免死锁。
内容的提问来源于stack exchange,提问作者DDStackoverflow
相关产品推荐
相关产品推荐

