理解Go的并发编程,得先弄明白什么是死锁。
这事说起来简单,碰到却头疼。并发编程里的死锁,说的是几个 Goroutine 拿着一堆资源,互相等对方放手,结果谁也不干活,程序就僵住了。
实际情况里,很多人上手写并发,调着调着发现程序莫名其妙地停了,也不知道卡在哪儿。九成九,你这是碰上了死锁。
举个例子,A和B两个人一起吃饭,但只有一双筷子。A拿到左手那根,想拿右手的,可这时B拿了右手的,想拿左手的。两个人都不愿放手,饭吃不成,事也别想干。
放在程序里,Goroutine A 拿着锁L1,想拿锁L2。而与此同时,Goroutine B 已经拿了锁L2,等着锁L1。好,这下谁也不用往前走了,整个流程就停住了。
找到死锁是个麻烦事,但解法有。我们得遵循一些简单的规矩。
首先,保证拿锁的顺序一致。大家都按照L1、L2的顺序拿,问题少一半。若是乱七八糟地争抢,注定出问题。
这时候你要明确每一步资源的争夺顺序,各自排好队,到谁了谁拿。
另一个常见的法子,使用通道(channel)而非锁。这个管用,真管用。Goroutine 可以靠 channel 沟通,别让他们自己扛锁。Channel 自己有锁的机制,你一用它,这些事它帮你打理了。
说实话,这一套让我写Go的时候省了不少心。用上 channel 你不一定躲开所有坑,但是,你和死锁碰面的机会减少大半。简单地说,先看看你的设计里能不能重构一下,借道 channel,不要贪方便随意搞锁。
假如你的程序架构确实需要多个锁,再考虑到前面说的顺序一致,可再加一个方法,定时释放锁。你不妨写点逻辑,Goroutine拿到锁,先做个计时,超时就强制放弃自己手里的东西。你不走,别人也别想走,这样容易拖死整场局。
还有一个隐蔽的死锁场景——死等消息。假设A等B通过channel发消息,B也在等A的消息,这种也陷进去了。不解决这问题,就陷入循环等待,典型的死锁样式。关键时刻,用select加上default分支,别让它们干巴巴等着。有时候不留神,各种奇怪的现象就能冒头。
最后提一点,生产代码没事别搞得那么复杂。有些人写并发,爱炫技,什么错综复杂的锁和channel齐飞,逻辑缠绕得似一团乱麻。小心翼翼对自己说:简单点,它们简单点,往往最安全,省事的路子藏着不易察觉的快活。