golang--通道和锁

发布于:2025-07-30 ⋅ 阅读:(24) ⋅ 点赞:(0)

golang–通道和锁

在 Go 语言中,通道(Channel)锁(Mutex/RWMutex) 都是处理并发的核心工具,但它们的适用场景有本质区别。选择的关键在于问题本质是数据传递还是状态保护

一、何时使用锁(Mutex/RWMutex)?

核心场景:保护共享内存中的临界状态
当多个 goroutine 需要读写同一块内存数据时,使用锁确保状态一致性。

典型使用场景:
  1. 共享数据结构的保护

    type SafeCounter struct {
        mu    sync.Mutex
        count int
    }
    
    func (c *SafeCounter) Inc() {
        c.mu.Lock()
        defer c.mu.Unlock()
        c.count++ // 修改共享状态
    }
    
  2. 配置热更新
    多个 goroutine 读取全局配置,偶尔需要更新:

    var (
        config atomic.Value // 或 Mutex + struct
        mu     sync.RWMutex
    )
    
    func UpdateConfig(newCfg Config) {
        mu.Lock()
        defer mu.Unlock()
        globalConfig = newCfg
    }
    
  3. 缓存系统(读多写少用 RWMutex

    var cache struct {
        mu   sync.RWMutex
        data map[string]string
    }
    
    func Get(key string) string {
        cache.mu.RLock()
        defer cache.mu.RUnlock()
        return cache.data[key]
    }
    
  4. 资源池管理(如数据库连接池)
    分配和回收资源时需要互斥操作。


二、何时使用通道(Channel)?

核心场景:协调 goroutine 间的协作与通信
当需要传递数据、发送信号或编排工作流时,通道是更符合 Go 哲学的选择。

典型使用场景:
  1. 流水线(Pipeline)处理

    func producer() <-chan int {
        ch := make(chan int)
        go func() {
            for i := 0; i < 10; i++ {
                ch <- i // 传递数据
            }
            close(ch)
        }()
        return ch
    }
    
    func consumer(input <-chan int) {
        for n := range input {
            fmt.Println(n) // 处理数据
        }
    }
    
  2. 任务分发/工作池模式

    func worker(taskCh <-chan Task, resultCh chan<- Result) {
        for task := range taskCh {
            resultCh <- process(task) // 分发任务并收集结果
        }
    }
    
  3. 事件通知与信号传递(推荐用 chan struct{}

    done := make(chan struct{})
    go func() {
        // ... 执行任务
        close(done) // 广播结束信号(零内存开销)
    }()
    
    <-done // 等待结束
    
  4. 超时控制

    select {
    case res := <-dataCh:
        use(res)
    case <-time.After(3 * time.Second):
        log.Println("timeout")
    }
    
  5. 多路复用(Multiplexing)

    select {
    case msg1 := <-ch1:
        handle(msg1)
    case msg2 := <-ch2:
        handle(msg2)
    }
    

三、关键对比总结

特性 锁(Mutex) 通道(Channel)
核心目的 保护共享内存状态 goroutine 间通信与协作
数据流动 无(原地修改) 有(数据在 goroutine 间传递)
阻塞行为 争用失败时阻塞 发送/接收时阻塞(根据缓冲情况)
适用模式 共享内存模型 CSP 模型(通信顺序进程)
典型场景 计数器、缓存、配置 流水线、工作池、事件总线
性能考量 低延迟(临界区小时) 有调度开销(但更安全)
错误处理 需手动防止死锁 可通过 close 广播信号

四、实际案例对比

场景:实现并发安全的计数器

方案1:用锁(适合简单状态)

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Add(n int) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value += n
}

方案2:用通道(过度设计,仅演示)

type Counter struct {
    ch chan int
}

func NewCounter() *Counter {
    c := &Counter{ch: make(chan int)}
    go c.run() // 后台goroutine管理状态
    return c
}

func (c *Counter) run() {
    var count int
    for delta := range c.ch {
        count += delta // 所有修改串行化处理
    }
}

func (c *Counter) Add(n int) {
    c.ch <- n
}

结论:计数器是典型的状态保护问题,锁更简单高效。通道方案虽然线程安全,但引入了不必要的复杂性和 goroutine 开销。


五、Go 箴言指导

“Do not communicate by sharing memory; instead, share memory by communicating.”
不要通过共享内存来通信;而应通过通信来共享内存。

决策建议:
  1. 优先考虑通道

    • 当问题涉及 goroutine 间协作、数据流动或生命周期管理时
    • 例如:任务分发、流水线、事件驱动架构
  2. 合理使用锁

    • 当只需保护少量共享状态(如计数器、标志位)
    • 性能敏感且临界区极小的场景
    • 实现线程安全的数据结构(如 sync.Map 内部使用锁)
  3. 混合使用(常见模式):

    var (
        cacheMu sync.RWMutex                  // 用锁保护缓存
        cache   map[string]interface{}
        
        refreshCh = make(chan struct{}, 1)    // 用通道触发更新
    )
    
    // 后台刷新协程
    go func() {
        for range refreshCh {                 // 接收刷新信号
            updateCache()                     // 内部用锁保护更新
        }
    }()
    

六、需要避免的陷阱

  1. 用通道模拟锁

    // 反模式:用容量1的通道模拟互斥锁
    var sem = make(chan struct{}, 1)
    func Inc() {
        sem <- struct{}{}  // P操作
        count++            // 临界区
        <-sem              // V操作
    }
    

    问题:不如直接使用 sync.Mutex 清晰高效(标准库锁经过充分优化)。

  2. 在通道中传递互斥锁

    ch <- &sync.Mutex{} // 危险!锁状态不可复制
    

    规则:锁必须通过指针传递,且禁止复制。

  3. 忽视通道关闭规则

    • 向已关闭通道发送数据会 panic
    • 重复关闭通道会 panic
      最佳实践:由发送方负责关闭,并用 sync.Once 或上下文控制关闭时机。

总结:核心决策原则

问题类型 解决方案 原因
保护共享变量状态 直接控制内存访问
goroutine 间传递数据 通道 安全的数据载体
通知事件/信号 通道 close(ch) 是高效的广播机制
超时/多路操作 通道 + select 原生支持多路复用
实现复杂工作流(如Pipeline) 通道 自然表达数据流动

黄金法则

  • 操作对象是 内存地址 → 用锁 🔒
  • 操作对象是 行为协调 → 用通道 📨

网站公告

今日签到

点亮在社区的每一天
去签到