
Go语言加锁代码偶尔出现panic: send on closed channel的原因分析
在Go语言并发编程中,使用锁(mutex)保证线程安全是常见做法,但即使使用了锁,仍然可能遇到panic: send on closed channel错误。本文分析此问题出现的原因及解决方案。
问题代码及现象
以下代码片段演示了该问题:
package main
import (
"context"
"fmt"
"sync"
)
var lock sync.Mutex
func main() {
c := make(chan int, 10)
wg := sync.WaitGroup{}
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go func() {
defer wg.Done()
lock.Lock()
cancel()
close(c)
lock.Unlock()
}()
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
select {
case c <- i:
fmt.Printf("sent %d\n", i)
case <-ctx.Done():
fmt.Println("context cancelled")
}
}(i)
}
wg.Wait()
}
尽管使用了lock.Lock()和lock.Unlock()保护临界区,但程序仍然可能在c 处panic,因为select语句的非确定性行为。
问题分析
Go语言select语句具有非确定性:如果多个case都准备好接收或发送,select会随机选择一个执行。
关键在于:
-
close(c)和c 的竞争:close(c)操作和c 操作并非原子操作,存在竞争条件。即使加了锁,close(c)操作可能在c 操作之后执行,导致c 尝试向已关闭的通道发送数据,从而引发panic。 -
select语句的随机性: 即使ctx.Done()已经准备好,select仍然可能随机选择c 执行。
解决方案
为了避免此问题,需要确保在发送数据前检查通道是否已关闭。 可以使用select语句的默认case来实现:
select {
case c <- i:
fmt.Printf("sent %d\n", i)
default:
fmt.Println("channel closed or full")
}
或者,使用一个额外的通道来协调关闭操作:
package main
import (
"fmt"
"sync"
)
func main() {
c := make(chan int, 10)
done := make(chan struct{})
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
close(done) // Signal that the channel is closing
close(c)
}()
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
select {
case c <- i:
fmt.Printf("sent %d\n", i)
case <-done:
fmt.Println("channel closing")
}
}(i)
}
wg.Wait()
}
这个改进的版本使用done通道来通知goroutine通道即将关闭,避免了竞争条件。
通过以上方法,可以有效地避免panic: send on closed channel错误,即使在并发环境下使用锁。 选择哪种解决方案取决于具体的应用场景和代码复杂度。










