Go language channel blocking depth resolution: from DeadLock to asynchronous processing
In the concurrent programming of the Go language, the channel (channel) is the bridge connecting the goroutine. Many beginners often encounter “deadlock” errors when using unbuffered channels, or are confused about the blocking mechanism of the channel.
This article will use a specific case to compare two different channel usage scenarios, and deeply analyze the blocking principle of the Go channel and the logic of the main thread exit.
1. Case background
We simulate a classic producer-consumer model:
1. WriteData Coroutines: Responsible for writing data to INTCHAN.
2. ReadData Coroutines: Responsible for reading data from IntChan.
3. ExitChan Channel: used to notify the main thread that all tasks have been completed.
Reference: GO Review Notes: Goroutine and Channel work together (solve locks and sleep hidden dangers)
package main
import (
"fmt"
// 此处无需引入time包,彻底摆脱手动休眠的依赖
)
// writeData 协程:向intChan写入50个整数(生产者)
func writeData(intChan chan int) {
// 循环写入50个整数,逻辑简单但需注意循环边界
for i := 1; i <= 50; i++ {
// 向管道写入数据,有缓冲管道会自动调节写入节奏
intChan <- i
fmt.Println("writeData ", i)
// 此处可注释time.Sleep,验证Channel的缓冲特性,无需手动控制写入速度
// time.Sleep(time.Second)
}
// 关键操作:写入完成后关闭管道,告知readData协程“数据已写完”
close(intChan)
}
// readData 协程:从intChan读取数据,读取完成后向exitChan发送信号(消费者)
func readData(intChan chan int, exitChan chan bool) {
// 无限循环读取管道数据,直到管道关闭且无数据可读
for {
// 核心语法:v接收数据,ok判断管道是否关闭(true=有数据/未关闭,false=管道关闭且无数据)
v, ok := <-intChan
if !ok { // 管道关闭且无数据,说明读取完成,退出循环
break
}
fmt.Printf("readData 读到数据=%v\n", v)
}
// readData 读取完数据后,即任务完成
exitChan <- true
// 关闭exitChan(可选,此处关闭是为了让主线程读取信号后正常退出,避免阻塞)
close(exitChan)
}
func main() {
// 创建两个管道
// 1. 创建数据管道intChan:有缓冲管道,容量设为10,平衡读写节奏
// 复盘:容量可根据实际需求调整,此处10足够承载writeData的写入速度,避免频繁阻塞
intChan := make(chan int, 10)
// 2. 创建信号管道exitChan:用于传递“协程完成”信号,容量设为1即可(仅需传递1个信号)
exitChan := make(chan bool, 1)
// 启动两个协程,共享同一个intChan,实现协同工作
go writeData(intChan)
go readData(intChan, exitChan)
// 主线程监听exitChan,等待readData协程发送完成信号,精准退出
// 复盘:此处替代了此前的time.Sleep,彻底解决休眠时间估算不准的问题
for {
_, ok := <-exitChan
if !ok { // exitChan关闭且无信号,说明所有协程都已完成
break
}
}
// 主线程退出前可添加提示,验证协同效果
fmt.Println("所有协程执行完成,主线程正常退出")
}
2. Scenario 1: Deadlock caused by the lack of consumers
First look at the first case, we only start the write coroutine in the main function, and comment out the read coroutine:
// Go ReadData(Intchan, ExitChan)
The running result is as follows:
/app/go-atguigu/channel-apply # go run main.go
writeData 1
writeData 2
writeData 3
writeData 4
writeData 5
writeData 6
writeData 7
writeData 8
writeData 9
writeData 10
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/app/go-atguigu/channel-apply/main.go:54 +0x8b
goroutine 7 [chan send]:
main.writeData(0x2e915bc56000)
/app/go-atguigu/channel-apply/main.go:13 +0x39
created by main.main in goroutine 1
/app/go-atguigu/channel-apply/main.go:48 +0x7c
exit status 2
Cause analysis:
1. Characteristics of unbuffered channels: IntChan is an unbuffered channel. This means that the sending operation (write) and the receiving operation (reading) must be prepared at the same time to complete. If there is only the sender and no receiver, the sender will block permanently.
2. Blocking of the writer: WriteData coroutines write the 11th data to the channel (or when the buffer is full or no receiver), since there is no readData The coroutine is received at the other end and it is blocked on the sending operation.
3. Blocking of the main thread: The main thread is executing <-exitChan and waiting for the exit signal. Since WriteData is blocked, it can never perform the step of closing the channel or sending an exit signal; and ReadData is not activated at all, and naturally it will not send a signal.
4. Deadlock detection: All active goroutines (main thread and writeData) are detected at runtime when the Go runs, and there is no possibility of being awakened, so it throws a fatal error: all goroutines are asleep – Deadlock!.
Conclusion: When using the non-buffered channel, you must ensure that there is a corresponding receiver running, otherwise the sending operation will cause the program to crash.
3. Scenario 2: Asynchronous processing of mismatched read and write speeds
In the second case, we started the ReadData coroutine normally, but added time.sleep(time.second) to the read logic to simulate the scenario of “fast write and slow read”.
The running result is as follows:
/app/go-atguigu/channel-apply # go run main.go
writeData 1
writeData 2
writeData 3
writeData 4
readData 读到数据=1
writeData 5
writeData 6
writeData 7
writeData 8
writeData 9
writeData 10
writeData 11
readData 读到数据=2
writeData 12
readData 读到数据=3
writeData 13
readData 读到数据=4
writeData 14
readData 读到数据=5
writeData 15
readData 读到数据=6
writeData 16
readData 读到数据=7
writeData 17
readData 读到数据=8
writeData 18
readData 读到数据=9
writeData 19
readData 读到数据=10
writeData 20
readData 读到数据=11
writeData 21
readData 读到数据=12
writeData 22
readData 读到数据=13
writeData 23
readData 读到数据=14
writeData 24
readData 读到数据=15
writeData 25
readData 读到数据=16
writeData 26
readData 读到数据=17
writeData 27
readData 读到数据=18
writeData 28
readData 读到数据=19
writeData 29
readData 读到数据=20
writeData 30
readData 读到数据=21
writeData 31
readData 读到数据=22
writeData 32
readData 读到数据=23
writeData 33
readData 读到数据=24
writeData 34
readData 读到数据=25
writeData 35
readData 读到数据=26
writeData 36
readData 读到数据=27
writeData 37
readData 读到数据=28
writeData 38
readData 读到数据=29
writeData 39
readData 读到数据=30
writeData 40
readData 读到数据=31
writeData 41
readData 读到数据=32
writeData 42
readData 读到数据=33
writeData 43
readData 读到数据=34
writeData 44
readData 读到数据=35
writeData 45
readData 读到数据=36
writeData 46
readData 读到数据=37
writeData 47
readData 读到数据=38
writeData 48
readData 读到数据=39
writeData 49
readData 读到数据=40
writeData 50
readData 读到数据=41
readData 读到数据=42
readData 读到数据=43
readData 读到数据=44
readData 读到数据=45
readData 读到数据=46
readData 读到数据=47
readData 读到数据=48
readData 读到数据=49
readData 读到数据=50
所有协程执行完成,主线程正常退出
Phenomenon interpretation:
1. Why is there no deadlock?
Although the read is slow, the ReadData coroutine is indeed running. Whenever ReadData takes out a data from the channel, the blocking of the WriteData will be released, thereby continuing to write the next data. This “handshake” mechanism ensures the flow of the program.
2. The mystery of the output order:
You will find that the output is not strictly “write one, read one”. This is because writeData is much faster than ReadData.
– WriteData will write data quickly and try to write to the next. If the previous data has not been taken away, it will block and wait at the channel transmission.
– Once the readdata wakes up and takes the data, the writedata is immediately restored and may write several more data in an instant until it blocks again.
– This alternate visual effect reflects the switching of the Go scheduler between different goroutines.
3. Why does the main thread exit normally?
The key is ExitChan.
– When WriteData has written all the data, it will close the IntChan.
– READDATA is closed through the range or detection channel, and after processing all the remaining data, send a signal to the EXITCHAN.
– After the main thread receives the signal of <-exitChan, it no longer blocks, and the subsequent code is successfully executed and exited.
4. Summary of core knowledge points
1. Synchronization and communication:
The bufferless channel is not only a data transmission pipeline, but also a synchronization tool. It enforces the sender and receiver to “meet” at the same time.
2. The essence of deadlock:
Deadlock usually occurs when “looping waits” or “all coroutines are waiting for events that are not possible”. In channel operations, the most common cause is the lack of a paired receiver or sender.
3. Production environment recommendations:
– Avoid directly blocking channels operations in the main thread, unless you are sure that there is a back-end coroutine in cooperation.
– If you want to decouple the speed of production and consumption, consider using a buffered channel (make(chan int, buffer_size)), so that the producer can continuously write before the buffer is full, increasing the throughput of concurrent.
– Always design the exit mechanism (such as using ExitChan or Context.Context) to prevent cordification from leaking or the main thread unlimited wait.
5. Conclusion
Through this simple comparison experiment, we can see the power and rigor of the Go language channel mechanism. Understanding “blocking” is not a bad thing, it is the basis for Go’s efficient concurrent synchronization. As long as it is properly matched with goroutine and channel, you can build a safe and efficient concurrent program.