Go 并发陷阱
Go并发陷阱[编辑 | 编辑源代码]
介绍[编辑 | 编辑源代码]
Go语言以其轻量级的goroutine和高效的并发模型著称,但在实际开发中,开发者常会遇到一些并发陷阱。这些陷阱可能导致数据竞争、死锁、资源泄漏等问题。本章节将详细介绍常见的Go并发陷阱,帮助初学者和高级开发者避免这些错误。
常见并发陷阱[编辑 | 编辑源代码]
1. 数据竞争(Data Race)[编辑 | 编辑源代码]
数据竞争发生在多个goroutine同时访问同一块内存,且至少有一个是写入操作时。Go提供了`-race`标志来检测数据竞争。
示例代码[编辑 | 编辑源代码]
package main
import (
"fmt"
"sync"
)
var counter int
var wg sync.WaitGroup
func increment() {
defer wg.Done()
counter++
}
func main() {
wg.Add(2)
go increment()
go increment()
wg.Wait()
fmt.Println("Counter:", counter)
}
输出与问题[编辑 | 编辑源代码]
由于`counter++`不是原子操作,两个goroutine可能同时读取和写入`counter`,导致结果不确定。 可能的输出:
Counter: 1 // 预期是2,但因数据竞争导致结果错误
解决方法[编辑 | 编辑源代码]
使用`sync.Mutex`或`sync/atomic`包实现同步:
var mutex sync.Mutex
func increment() {
defer wg.Done()
mutex.Lock()
counter++
mutex.Unlock()
}
2. 死锁(Deadlock)[编辑 | 编辑源代码]
死锁指多个goroutine互相等待对方释放资源,导致程序无法继续执行。
示例代码[编辑 | 编辑源代码]
package main
import (
"sync"
)
var mu1, mu2 sync.Mutex
func goroutine1() {
mu1.Lock()
defer mu1.Unlock()
mu2.Lock()
defer mu2.Unlock()
}
func goroutine2() {
mu2.Lock()
defer mu2.Unlock()
mu1.Lock()
defer mu1.Unlock()
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() { goroutine1(); wg.Done() }()
go func() { goroutine2(); wg.Done() }()
wg.Wait()
}
问题分析[编辑 | 编辑源代码]
`goroutine1`和`goroutine2`分别持有`mu1`和`mu2`,并尝试获取对方的锁,导致死锁。
解决方法[编辑 | 编辑源代码]
- 按固定顺序获取锁。 - 使用`sync.RWMutex`或超时机制(如`context.WithTimeout`)。
3. Goroutine泄漏[编辑 | 编辑源代码]
Goroutine泄漏指goroutine因未正确退出而长期占用资源。
示例代码[编辑 | 编辑源代码]
package main
import (
"time"
)
func leakyFunc(ch chan int) {
val := <-ch // 阻塞,但外部可能忘记发送数据
println(val)
}
func main() {
ch := make(chan int)
go leakyFunc(ch)
time.Sleep(1 * time.Second)
// 忘记关闭或发送数据,goroutine永远阻塞
}
解决方法[编辑 | 编辑源代码]
- 使用带缓冲的通道或`select`超时:
select {
case val := <-ch:
println(val)
case <-time.After(500 * time.Millisecond):
println("Timeout")
}
4. 通道误用[编辑 | 编辑源代码]
通道是Go并发的核心,但误用会导致问题。
未关闭的通道[编辑 | 编辑源代码]
未关闭的通道可能导致接收者永久阻塞。
解决方案[编辑 | 编辑源代码]
- 由发送方关闭通道。 - 使用`defer close(ch)`确保关闭。
关闭已关闭的通道[编辑 | 编辑源代码]
重复关闭通道会引发panic。
解决方案[编辑 | 编辑源代码]
- 使用`sync.Once`确保只关闭一次。
实际案例[编辑 | 编辑源代码]
案例:Web请求限流[编辑 | 编辑源代码]
假设需要限制并发请求数,避免资源耗尽。
代码实现[编辑 | 编辑源代码]
package main
import (
"sync"
"time"
)
func worker(id int, limiter chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
limiter <- struct{}{} // 占用令牌
defer func() { <-limiter }() // 释放令牌
time.Sleep(1 * time.Second)
println("Worker", id, "done")
}
func main() {
limiter := make(chan struct{}, 3) // 限制3个并发
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, limiter, &wg)
}
wg.Wait()
}
输出[编辑 | 编辑源代码]
Worker 1 done Worker 2 done Worker 3 done Worker 4 done Worker 5 done
总结[编辑 | 编辑源代码]
Go并发编程需注意以下陷阱: 1. 使用`-race`检测数据竞争。 2. 避免锁的循环依赖(死锁)。 3. 确保goroutine能正常退出。 4. 正确管理通道的生命周期。
通过理解这些陷阱,开发者可以编写更健壮的并发程序。