跳转到内容

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. 正确管理通道的生命周期。

通过理解这些陷阱,开发者可以编写更健壮的并发程序。

graph TD A[并发陷阱] --> B[数据竞争] A --> C[死锁] A --> D[Goroutine泄漏] A --> E[通道误用] B --> F[使用Mutex/Atomic] C --> G[固定锁顺序] D --> H[超时/Context] E --> I[正确关闭通道]