3、🚫 别再用 Mutex 了!Go 中优雅的并发控制姿势

67 阅读3分钟

如果你还在用 sync.Mutex,那你就错过了 Go 并发最性感的部分:channel。
在 Go 世界,锁只是基础款,channel 才是艺术。


🧩 一、为什么大家讨厌锁?

先来看一段“经典死锁”事故现场:

var mu sync.Mutex

func f() {
    mu.Lock()
    defer mu.Unlock()
    // 某些操作...
}

问题不大,对吧?但加上 goroutine 和多函数嵌套调用后,就变成这样:

  • 谁先加锁?
  • 谁忘记解锁?
  • 是不是加了两次?
  • 会不会卡死整段服务?

用锁像谈恋爱:开始简单,后来复杂、误解、崩溃。


📦 二、Go 提供了更优雅的办法:channel

Channel 是 Go 原生的通信机制,核心理念:不要通过共享内存来通信,而要通过通信来共享内存。

它能做什么?

  • 多个协程之间传值
  • 控制并发流量(像限流阀)
  • 实现任务队列工作池

🧪 三、channel 实战演示

🌰 场景1:两个协程同步执行

func main() {
    done := make(chan bool)

    go func() {
        fmt.Println("goroutine: 你好!")
        done <- true
    }()

    <-done
    fmt.Println("main: 完成")
}

channel 在这里就像一个“信号枪”:通知任务完成。


🔁 场景2:worker 池实现

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d 处理 job %d\n", id, j)
        time.Sleep(time.Second)
        results <- j * 2
    }
}
func main() {
    jobs := make(chan int, 5)
    results := make(chan int, 5)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= 5; j++ {
        jobs <- j
    }
    close(jobs)

    for r := 1; r <= 5; r++ {
        <-results
    }
}

✅ 优势:

  • 自动调度
  • 没有竞态
  • 易于水平扩展

⛏ 四、channel 与 Mutex 的对比

特性channelsync.Mutex
学习成本略高
可视化程度明确传值隐式状态
死锁风险低(但不是没有)高(易忘解锁)
用途通信 + 同步只同步

结论:channel 更适合 并发编排、任务流转、协程之间的数据交换,不是万能,但更优雅。


🧠 五、select:并发中的“调度大师”

select {
case msg1 := <-ch1:
    fmt.Println("接收 ch1:", msg1)
case msg2 := <-ch2:
    fmt.Println("接收 ch2:", msg2)
default:
    fmt.Println("都没有数据")
}

select 可以非阻塞地监听多个 channel,非常适合 多路监听、超时控制、优雅退出


🔥 六、限流器示例:控制并发节奏

limit := make(chan struct{}, 10) // 同时最多10个

for i := 0; i < 100; i++ {
    limit <- struct{}{} // 占位
    go func(i int) {
        defer func() { <-limit }() // 释放
        doSomething(i)
    }(i)
}

这种方式比 “手动统计 goroutine 数量 + sleep” 优雅太多!


😰 七、channel 也不是没缺点

  • 无缓存 channel 容易阻塞,导致死等
  • 写 channel 时没有人读取会 panic
  • select 嵌套太多时可读性差
  • 滥用 channel 可能导致性能下降(不如锁快)

正确使用 channel 的前提:清晰的协程通信逻辑 + 控制 channel 生命周期


✅ 总结一下:

应用场景建议用法
数据共享 + 高性能sync.Mutex / atomic
协程通信、任务分发、信号传递channel
多任务监听、退出控制select + channel
高并发下限流buffered channel

🎬 下集预告

下一篇,我们将带你进入 Go 的“看不见的战场”——逃逸分析与垃圾回收,看看你写的每一行代码,是不是偷偷把内存“泄露”了。