golang--通道和锁
golang–通道和锁
在 Go 语言中,通道(Channel) 和 锁(Mutex/RWMutex) 都是处理并发的核心工具,但它们的适用场景有本质区别。选择的关键在于问题本质是数据传递还是状态保护。
一、何时使用锁(Mutex/RWMutex)?
核心场景:保护共享内存中的临界状态
当多个 goroutine 需要读写同一块内存数据时,使用锁确保状态一致性。
典型使用场景:
-
共享数据结构的保护
type SafeCounter struct {mu sync.Mutexcount int }func (c *SafeCounter) Inc() {c.mu.Lock()defer c.mu.Unlock()c.count++ // 修改共享状态 }
-
配置热更新
多个 goroutine 读取全局配置,偶尔需要更新:var (config atomic.Value // 或 Mutex + structmu sync.RWMutex )func UpdateConfig(newCfg Config) {mu.Lock()defer mu.Unlock()globalConfig = newCfg }
-
缓存系统(读多写少用
RWMutex
)var cache struct {mu sync.RWMutexdata map[string]string }func Get(key string) string {cache.mu.RLock()defer cache.mu.RUnlock()return cache.data[key] }
-
资源池管理(如数据库连接池)
分配和回收资源时需要互斥操作。
二、何时使用通道(Channel)?
核心场景:协调 goroutine 间的协作与通信
当需要传递数据、发送信号或编排工作流时,通道是更符合 Go 哲学的选择。
典型使用场景:
-
流水线(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) // 处理数据} }
-
任务分发/工作池模式
func worker(taskCh <-chan Task, resultCh chan<- Result) {for task := range taskCh {resultCh <- process(task) // 分发任务并收集结果} }
-
事件通知与信号传递(推荐用
chan struct{}
)done := make(chan struct{}) go func() {// ... 执行任务close(done) // 广播结束信号(零内存开销) }()<-done // 等待结束
-
超时控制
select { case res := <-dataCh:use(res) case <-time.After(3 * time.Second):log.Println("timeout") }
-
多路复用(Multiplexing)
select { case msg1 := <-ch1:handle(msg1) case msg2 := <-ch2:handle(msg2) }
三、关键对比总结
特性 | 锁(Mutex) | 通道(Channel) |
---|---|---|
核心目的 | 保护共享内存状态 | goroutine 间通信与协作 |
数据流动 | 无(原地修改) | 有(数据在 goroutine 间传递) |
阻塞行为 | 争用失败时阻塞 | 发送/接收时阻塞(根据缓冲情况) |
适用模式 | 共享内存模型 | CSP 模型(通信顺序进程) |
典型场景 | 计数器、缓存、配置 | 流水线、工作池、事件总线 |
性能考量 | 低延迟(临界区小时) | 有调度开销(但更安全) |
错误处理 | 需手动防止死锁 | 可通过 close 广播信号 |
四、实际案例对比
场景:实现并发安全的计数器
方案1:用锁(适合简单状态)
type Counter struct {mu sync.Mutexvalue 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 intfor 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.”
不要通过共享内存来通信;而应通过通信来共享内存。
决策建议:
-
优先考虑通道:
- 当问题涉及 goroutine 间协作、数据流动或生命周期管理时
- 例如:任务分发、流水线、事件驱动架构
-
合理使用锁:
- 当只需保护少量共享状态(如计数器、标志位)
- 性能敏感且临界区极小的场景
- 实现线程安全的数据结构(如
sync.Map
内部使用锁)
-
混合使用(常见模式):
var (cacheMu sync.RWMutex // 用锁保护缓存cache map[string]interface{}refreshCh = make(chan struct{}, 1) // 用通道触发更新 )// 后台刷新协程 go func() {for range refreshCh { // 接收刷新信号updateCache() // 内部用锁保护更新} }()
六、需要避免的陷阱
-
用通道模拟锁
// 反模式:用容量1的通道模拟互斥锁 var sem = make(chan struct{}, 1) func Inc() {sem <- struct{}{} // P操作count++ // 临界区<-sem // V操作 }
问题:不如直接使用
sync.Mutex
清晰高效(标准库锁经过充分优化)。 -
在通道中传递互斥锁
ch <- &sync.Mutex{} // 危险!锁状态不可复制
规则:锁必须通过指针传递,且禁止复制。
-
忽视通道关闭规则
- 向已关闭通道发送数据会 panic
- 重复关闭通道会 panic
最佳实践:由发送方负责关闭,并用sync.Once
或上下文控制关闭时机。
总结:核心决策原则
问题类型 | 解决方案 | 原因 |
---|---|---|
保护共享变量状态 | 锁 | 直接控制内存访问 |
goroutine 间传递数据 | 通道 | 安全的数据载体 |
通知事件/信号 | 通道 | close(ch) 是高效的广播机制 |
超时/多路操作 | 通道 + select | 原生支持多路复用 |
实现复杂工作流(如Pipeline) | 通道 | 自然表达数据流动 |
黄金法则:
- 操作对象是 内存地址 → 用锁 🔒
- 操作对象是 行为协调 → 用通道 📨