在Golang的并发编程场景中,死锁是指两个或两个以上的goroutine在执行过程中,因争夺资源或者彼此等待对方释放资源而陷入无限阻塞的状态,程序会直接卡住无法继续向下执行,严重时会导致整个服务不可用。

Go语言死锁产生的常见原因
1. channel读写顺序不匹配
无缓冲channel的读写是同步的,如果只有发送方没有接收方,或者只有接收方没有发送方,就会触发死锁。比如下面这段代码的channel是无缓冲的,主goroutine在发送数据后没有接收方,就会陷入阻塞。
package main
func main() {
ch := make(chan int)
// 向无缓冲channel发送数据,没有接收方,会触发死锁
ch <- 1
}
2. 多个goroutine互相等待资源
当多个goroutine分别持有对方需要的锁或者资源,彼此等待对方释放时,就会产生死锁。比如两个goroutine分别先获取锁A和锁B,再尝试获取对方已经持有的锁,就会陷入互相等待的状态。
package main
import (
"sync"
"time"
)
func main() {
var lockA, lockB sync.Mutex
// 第一个goroutine先获取lockA,再尝试获取lockB
go func() {
lockA.Lock()
time.Sleep(time.Second)
lockB.Lock()
lockB.Unlock()
lockA.Unlock()
}()
// 第二个goroutine先获取lockB,再尝试获取lockA
lockB.Lock()
time.Sleep(time.Second)
lockA.Lock()
lockA.Unlock()
lockB.Unlock()
}
3. 忘记关闭channel导致接收方永久阻塞
如果发送方发送完数据后没有关闭channel,接收方在接收完所有数据后会一直等待新的数据,从而产生死锁。比如下面代码中发送方只发送了一个数据,没有关闭channel,接收方在接收完数据后会继续阻塞等待。
package main
import "time"
func main() {
ch := make(chan int)
go func() {
ch <- 1
// 没有关闭channel,接收方会一直等待
}()
<-ch
// 接收完数据后,主goroutine没有其他逻辑,但是channel未关闭,不会触发死锁
// 如果这里还有第二次接收操作,就会触发死锁
time.Sleep(time.Second)
}
避免Golang死锁的实用方案
1. 合理设计channel的读写逻辑
使用无缓冲channel时,要确保发送和接收操作都有对应的goroutine执行,避免单边操作。如果不确定接收方是否存在,可以使用带缓冲的channel,或者先判断channel是否可写。另外,发送方在发送完所有数据后要及时关闭channel,避免接收方永久阻塞。
package main
func main() {
// 使用带缓冲的channel,避免发送时直接阻塞
ch := make(chan int, 1)
ch <- 1
// 接收数据后关闭channel
val := <-ch
close(ch)
println(val)
}
2. 统一锁的获取顺序
当多个goroutine需要获取多把锁时,要约定统一的获取顺序,所有goroutine都按照相同的顺序获取锁,就可以避免互相等待的情况。比如上面的死锁示例,只要两个goroutine都先获取lockA再获取lockB,就不会产生死锁。
package main
import (
"sync"
"time"
)
func main() {
var lockA, lockB sync.Mutex
// 两个goroutine都先获取lockA,再获取lockB
go func() {
lockA.Lock()
time.Sleep(time.Second)
lockB.Lock()
lockB.Unlock()
lockA.Unlock()
}()
lockA.Lock()
time.Sleep(time.Second)
lockB.Lock()
lockB.Unlock()
lockA.Unlock()
}
3. 使用select语句设置超时机制
在从channel接收数据或者向channel发送数据时,可以使用select搭配time.After设置超时时间,避免goroutine无限阻塞。如果超过指定时间还没有完成读写操作,就执行超时逻辑,释放相关资源。
package main
import (
"time"
)
func main() {
ch := make(chan int)
select {
case val := <-ch:
println(val)
case <-time.After(time.Second * 2):
// 超时后执行逻辑,避免永久阻塞
println("接收数据超时")
}
}
4. 利用工具检测死锁
Go语言自带的go vet工具可以检测部分常见的死锁问题,另外在开发阶段可以开启race检测,运行程序时添加-race参数,能够帮助发现并发场景下的资源竞争和潜在的死锁风险。对于复杂的并发逻辑,也可以先通过单元测试模拟并发场景,提前排查死锁问题。
死锁排查的小技巧
当程序出现死锁时,可以先查看程序的报错信息,Go的死锁报错会提示哪些goroutine被阻塞,以及阻塞的位置。如果是生产环境的问题,可以打印goroutine的堆栈信息,分析各个goroutine的状态和持有的资源,定位死锁产生的原因。另外,简化并发逻辑,逐步注释掉部分代码,也能快速定位触发死锁的具体代码段。