Golang代码中常见的并发问题有哪些

Golang代码中常见的并发问题有哪些 在代码审查或现有代码和库中,你反复注意到哪些与并发相关的代码或设计层面的问题?

我注意到的一些情况:

  • time.Ticker 未被停止:缺少 defer tick.Stop()
  • 一个类型被设计为可供多个goroutine并发使用是安全的,但其文档没有说明哪些方法可以从哪些goroutine以及在何时调用。(默认情况可能是“任何方法都可以在任何时刻从任何goroutine调用”,但对于许多类型来说,这必须更具体,例如,某些方法必须仅从单个goroutine顺序调用。)
  • 与上一点相关/重叠:结构体中有一个 sync.Mutex 字段,但没有注释说明它应该保护哪些字段(特别是当字段超过1-2个,并且哪些是可变的、哪些是不可变的并不明显)以及原因。

(此帖转载自 Reddit。)


更多关于Golang代码中常见的并发问题有哪些的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang代码中常见的并发问题有哪些的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Golang并发编程中,确实存在一些常见问题。以下是我在实际代码审查中经常遇到的情况:

1. 竞态条件(Race Conditions)

最常见的问题是未使用同步机制保护共享数据:

// 错误示例
var counter int

func increment() {
    counter++ // 存在竞态条件
}

// 正确示例
var (
    counter int
    mu      sync.RWMutex
)

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

2. 通道使用不当

包括通道未关闭、阻塞操作等:

// 错误示例 - 通道泄漏
func process(ch chan int) {
    for i := 0; i < 10; i++ {
        go func(val int) {
            ch <- val * 2
        }(i)
    }
    // 通道从未关闭
}

// 正确示例
func process(ch chan int) {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(val int) {
            defer wg.Done()
            ch <- val * 2
        }(i)
    }
    go func() {
        wg.Wait()
        close(ch)
    }()
}

3. 资源泄漏

如你提到的 time.Ticker 问题:

// 错误示例
func startTicker() {
    ticker := time.NewTicker(time.Second)
    go func() {
        for range ticker.C {
            // 处理逻辑
        }
    }()
    // ticker 从未停止
}

// 正确示例
func startTicker() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop() // 确保停止
    
    go func() {
        for range ticker.C {
            // 处理逻辑
        }
    }()
}

4. 同步原语误用

sync.WaitGroupsync.Mutex 的常见错误:

// 错误示例 - WaitGroup 使用不当
func process() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        go func() {
            wg.Add(1) // 在 goroutine 中调用 Add
            defer wg.Done()
            // 工作
        }()
    }
    wg.Wait() // 可能过早返回
}

// 正确示例
func process() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1) // 在 goroutine 外调用 Add
        go func() {
            defer wg.Done()
            // 工作
        }()
    }
    wg.Wait()
}

5. 上下文(Context)传播问题

未正确传递或取消上下文:

// 错误示例
func handleRequest(ctx context.Context) {
    go func() {
        // 使用了外部上下文,但未处理取消
        result := longRunningTask(ctx)
        // ...
    }()
    // 函数返回后,goroutine 可能仍在运行
}

// 正确示例
func handleRequest(ctx context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()
    
    done := make(chan struct{})
    
    go func() {
        defer close(done)
        result := longRunningTask(ctx)
        // ...
    }()
    
    select {
    case <-ctx.Done():
        // 处理取消
    case <-done:
        // 正常完成
    }
}

6. 死锁

通常由锁的顺序问题引起:

// 错误示例 - 潜在的锁顺序死锁
type Service struct {
    mu1 sync.Mutex
    mu2 sync.Mutex
    data1 map[string]int
    data2 map[string]int
}

func (s *Service) Process() {
    s.mu1.Lock()
    s.mu2.Lock() // 如果另一个 goroutine 以相反顺序获取锁,可能死锁
    // ...
    s.mu2.Unlock()
    s.mu1.Unlock()
}

// 正确示例 - 使用一致的锁顺序
func (s *Service) Process() {
    // 总是先获取 mu1,再获取 mu2
    s.mu1.Lock()
    defer s.mu1.Unlock()
    
    s.mu2.Lock()
    defer s.mu2.Unlock()
    // ...
}

7. 原子操作误用

误用 sync/atomic 包:

// 错误示例
var config atomic.Value

func updateConfig() {
    cfg := &Config{Field: "value"}
    config.Store(cfg) // 存储指针,后续修改会影响已存储的值
    cfg.Field = "modified" // 这会影响已存储的配置
}

// 正确示例
func updateConfig() {
    cfg := &Config{Field: "value"}
    // 创建副本或确保不再修改
    config.Store(cfg)
    // 不再修改 cfg
}

这些问题通常可以通过以下方式避免:

  • 使用 go test -race 进行竞态检测
  • 使用 sync 包提供的同步原语
  • 明确文档记录并发安全要求
  • 使用 context 进行生命周期管理
  • 对共享数据进行适当的封装和保护
回到顶部