Go语言中为何加sync.RWMutex后channel输出为正序且写入正常?
关于sync.RWMutex与无缓冲channel协同的核心认知拆解
先明确两个不会变动的基础规则
所有疑问的根源,都来自对这两个特性的认知偏差:
sync.RWMutex的作用边界极其清晰:只会阻塞那些主动调用Lock()/RLock()尝试抢这把锁的goroutine。它根本不认识、也不会干预任何没主动碰这把锁的goroutine,更不会直接拦截channel的收发操作。你之前觉得“锁被占了,channel发送就会被挡住”是完全的认知错位——执行c <- i的first协程,从程序启动到结束从来没有尝试抢过这把锁,自然不会被Mutex影响。- 无缓冲channel的核心特性是:发送操作、接收操作必须两边同时处于就绪状态,才能完成一次数据传递;只要有一方没准备好,另一方就会永久阻塞等待。
带Mutex版本的真实执行时序
你之前梳理的执行逻辑在几个关键节点存在偏差,我们逐行还原实际的运行流程:
- 程序启动,main协程拉起
first协程进入循环,第一次迭代i=1:- 先执行
go second(dataSendChannel)启动第一个second协程。这个协程第一行就尝试调用rwMutex.Lock(),此时锁没有被任何goroutine持有,它会直接拿到锁,毫无阻碍走到下一行num := <-c,在channel接收端摆出等待姿势。 - 此时first协程刚要执行
c <- 1,发现接收端已经就绪,两边立刻配对完成数据传递,c <-1的阻塞状态解除,first协程才会进入下一次循环迭代。 - 拿到1的second协程继续执行:睡(10-1)=9秒、打印1、最后执行
rwMutex.Unlock()释放锁。
- 先执行
- 第一次循环走完,进入第二次迭代i=2:
- 执行
go second(...)启动第二个second协程。协程第一行尝试rwMutex.Lock()——此时锁已经被上一个second协程完全释放,它顺利拿到锁,走到接收端摆好姿势等待2。 - first协程的
c <-2顺利配对发送,协程继续睡8秒、打印2、解锁。
- 执行
- 后续i=3到i=10的循环,完全重复上面的节奏。
这里的核心关键是:所有second协程,想要走到channel接收的代码行,必须先拿到锁。而锁的释放时机,严格卡在“上一个协程完成接收、处理、打印的全流程”之后。这意味着channel的接收端永远一次只有一个goroutine在等待,根本不会出现多个接收者同时就绪的情况。
去掉Mutex后输出倒序的原因
移除Mutex后,所有second协程的执行路径不再有锁的拦截:
- first协程的循环会以极快的速度跑完10次迭代:每次启动一个second协程,这个协程第一时间就冲到
<-c的接收端等数据,配合无缓冲channel的配对特性,几毫秒之内就把1到10所有数字全部发送、接收完毕。 - 每个接收到数字的second协程,接下来会睡
(10 - num)秒:接10的协程睡0秒,立刻醒过来打印10;接9的协程睡1秒,醒了打印9……最后接1的协程要睡9秒,才会打印1。 - 最终的输出顺序和发送顺序没有任何关系,只和每个协程睡醒的时间有关,自然就是从10到1倒序排列。
对应你提出的核心疑问
- 为什么
c <- i从来不会被Mutex挡住?因为执行发送操作的first协程从来没有尝试抢这把锁,Mutex根本没有任何权限去阻塞它。就像你家门上的锁,只会挡住想开你家门的人,不会挡住楼下路过的人。 - Mutex是在channel接收到数据后才生效吗?不是。Mutex从second协程启动的第一行就开始生效,只是它的作用是“挡住后续来抢锁的second协程,让它们根本走不到channel接收的那行代码”,而不是拦截channel本身的收发。
- 为什么channel的表现会受Mutex影响?channel的行为从头到尾没有任何变化,变的是能站在channel接收端等待的goroutine数量。没有锁的时候,10个协程全部挤在接收端接完所有数据再各自处理,睡醒顺序乱了;有锁的时候,接收端一次只放一个协程进来,必须处理完上一个数据,才换下一个进来接下一个,自然输出顺序就顺了。
内容的提问来源于stack exchange,提问作者MiryangJung
相关产品推荐
相关产品推荐

