如何在Golang中处理协程死锁问题 优化锁和channel使用

来源:站长平台作者:泰国程序员头衔:程序员
导读:本期聚焦于小伙伴创作的《如何在Golang中处理协程死锁问题 优化锁和channel使用》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《如何在Golang中处理协程死锁问题 优化锁和channel使用》有用,将其分享出去将是对创作者最好的鼓励。

在Golang的并发编程场景中,协程死锁会导致程序卡住无法继续执行,这类问题大多和锁、channel的使用不当有关,需要从使用规范和场景设计两个层面去规避和解决。

如何在Golang中处理协程死锁问题 优化锁和channel使用

常见的协程死锁触发场景

锁相关的死锁场景

互斥锁是最容易引发死锁的同步工具,常见的问题包括加锁后未释放、嵌套加锁导致互相等待。

比如下面这段嵌套加锁的代码就会触发死锁:

package main

import (
	"fmt"
	"sync"
)

var mu1, mu2 sync.Mutex

func main() {
	// 第一个协程先加mu1再尝试加mu2
	go func() {
		mu1.Lock()
		fmt.Println("goroutine 1 get mu1")
		mu2.Lock() // 等待mu2释放
		fmt.Println("goroutine 1 get mu2")
		mu2.Unlock()
		mu1.Unlock()
	}()

	// 第二个协程先加mu2再尝试加mu1
	go func() {
		mu2.Lock()
		fmt.Println("goroutine 2 get mu2")
		mu1.Lock() // 等待mu1释放
		fmt.Println("goroutine 2 get mu1")
		mu1.Unlock()
		mu2.Unlock()
	}()

	select {} // 阻塞主协程避免程序退出
}

两个协程分别持有对方需要的锁,形成互相等待的状态,就会触发死锁。

channel相关的死锁场景

无缓冲channel的读写操作会互相阻塞,如果只进行发送没有接收者,或者只进行接收没有发送者,就会触发死锁。

比如下面这段无缓冲channel发送的示例:

package main

func main() {
	ch := make(chan int)
	ch <- 1 // 无缓冲channel发送,没有接收者,直接死锁
}

还有协程内接收无缓冲channel,但是没有发送者的情况,同样会导致死锁。

锁使用的优化方法

控制锁的粒度

锁的粒度越小,持有锁的时间越短,和其他协程冲突的概率就越低。尽量只把需要保护的共享资源操作放在加锁范围内,非共享资源的操作放到锁外面。

优化前的代码:

func updateData(mu *sync.Mutex, data *map[string]int, key string, value int) {
	mu.Lock()
	// 非共享资源的耗时操作放在锁内,增大锁粒度
	time.Sleep(time.Millisecond * 10)
	(*data)[key] = value
	mu.Unlock()
}

优化后的代码:

func updateData(mu *sync.Mutex, data *map[string]int, key string, value int) {
	// 耗时操作放到锁外面
	time.Sleep(time.Millisecond * 10)
	mu.Lock()
	(*data)[key] = value
	mu.Unlock()
}

避免嵌套加锁

如果必须加锁,尽量统一所有协程的加锁顺序,比如都按照锁的变量名字典序加锁,避免互相等待的情况。如果可以的话,尽量用读写锁sync.RWMutex代替互斥锁,读多写少的场景下能减少锁冲突。

使用defer确保锁释放

加锁后使用defer释放锁,能避免因为异常或者提前返回导致的锁未释放问题。

func safeLock(mu *sync.Mutex) {
	mu.Lock()
	defer mu.Unlock() // 无论函数怎么退出,都会执行释放锁操作
	// 业务逻辑
}

channel使用的优化方法

合理选择缓冲channel

如果发送和接收的速度不匹配,优先使用带缓冲的channel,缓冲大小根据实际场景设置,避免无缓冲channel的阻塞问题。但是要注意缓冲channel满了之后发送还是会阻塞,需要做好容量规划。

// 创建缓冲大小为10的channel,发送10次以内不会阻塞
ch := make(chan int, 10)

避免无接收者的发送操作

发送channel之前,确保已经有协程在接收,或者使用select配合default分支处理发送阻塞的情况,避免死锁。

func sendData(ch chan int, data int) {
	select {
	case ch <- data:
		fmt.Println("send success")
	default:
		fmt.Println("channel is full, skip send")
	}
}

关闭channel的规范

只由发送方关闭channel,不要由接收方关闭,也不要关闭已经关闭的channel,避免panic。接收方可以通过ok值判断channel是否已经关闭。

func receiveData(ch chan int) {
	for {
		data, ok := <-ch
		if !ok {
			fmt.Println("channel closed")
			return
		}
		fmt.Println("receive:", data)
	}
}

死锁问题的排查技巧

如果程序已经出现死锁,可以使用runtime包的NumGoroutine函数查看当前协程数量,或者使用pprof工具抓取协程栈信息,找到阻塞的协程位置,定位是锁还是channel导致的阻塞。另外Golang内置的死锁检测会在程序运行时抛出死锁的报错信息,里面会包含阻塞的协程调用栈,直接根据栈信息就能定位问题代码。

Golang协程死锁锁优化channel使用修改时间:2026-06-10 16:03:30

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。