通道并发保证:并发安全Memo缓存实现技术咨询
关于并发安全Memo缓存中通道的并发保证分析
先看你给出的这段Memo缓存实现代码:
package mu import ( "sync" ) // Func 表示可记忆化函数,针对字符串键操作,用于Mu类型 type Func func(key string) interface{} // Mu 是一个缓存,用于记忆昂贵计算的结果 // // 它采用传统的互斥锁实现。 type Mu struct { // 保护done字段 mu sync.RWMutex done map[string]chan bool memo map[string]interface{} f Func } // Get方法:若字符串键存在则返回对应值,否则计算值并缓存……
针对你咨询的通道done的并发保证逻辑,我来拆解下这个设计的核心思路和安全机制:
核心并发保证逻辑
这个设计是典型的「避免重复计算」并发缓存方案——当多个goroutine同时请求同一个未缓存的key时,只让其中一个去执行昂贵的计算f(key),其他goroutine等待计算完成后直接读取缓存结果,done通道就是用来同步这个等待过程的关键。
- 互斥锁守护共享结构:
mu读写锁用来保护done和memo两个map的所有读写操作,确保对这两个共享数据结构的访问是原子的,彻底避免并发读写map导致的panic或数据不一致问题。 - 通道作为同步广播信号:
- 第一个处理未缓存key的goroutine,会先创建对应key的
done通道,释放锁后执行计算; - 计算完成后,将结果写入
memo,再关闭done通道——通道关闭是一个广播信号,所有阻塞在这个通道上的goroutine都会被唤醒; - 后续请求该key的goroutine,发现
done通道存在时,会直接阻塞在通道上,直到收到完成信号,再从memo读取已计算好的结果。
- 第一个处理未缓存key的goroutine,会先创建对应key的
实现时需要注意的细节
- 锁的粒度优化:用RWMutex而非普通Mutex,是因为读操作(查询缓存、查询done通道)可以并发执行,只有写操作(创建done通道、写入memo、删除done通道)需要互斥,这能大幅提升高并发场景下的性能。
- 避免goroutine泄漏:必须确保每个创建的
done通道最终都会被关闭——哪怕计算f(key)发生panic,也要通过defer等方式关闭通道,否则等待的goroutine会永久阻塞。 - 双重检查避免竞态:在切换锁的过程中(比如从读锁切换到写锁),必须再次检查
done通道是否存在,防止在锁切换的间隙已经有其他goroutine启动了计算。
这里给你补一个符合这个设计的Get方法典型实现,能更直观看到通道的作用:
func (m *Mu) Get(key string) interface{} { m.mu.RLock() // 先查缓存,存在直接返回 res, ok := m.memo[key] if ok { m.mu.RUnlock() return res } // 查是否有其他goroutine在计算这个key done, ok := m.done[key] if ok { m.mu.RUnlock() <-done // 等待计算完成 m.mu.RLock() res = m.memo[key] m.mu.RUnlock() return res } m.mu.RUnlock() // 没有goroutine在计算,抢锁准备自己执行 m.mu.Lock() // 再次检查,防止切换锁的间隙已有其他goroutine创建了done通道 done, ok = m.done[key] if ok { m.mu.Unlock() <-done m.mu.RLock() res = m.memo[key] m.mu.RUnlock() return res } // 创建done通道,标记自己正在计算 done = make(chan bool) m.done[key] = done m.mu.Unlock() // 执行昂贵计算 res = m.f(key) // 计算完成,更新缓存并通知等待的goroutine m.mu.Lock() m.memo[key] = res delete(m.done, key) // 可选:计算完成后删除done通道释放内存 close(done) m.mu.Unlock() return res }
整体来看,这个结合互斥锁和通道的设计,既保证了并发安全,又避免了重复计算,是一种高效的并发缓存实现方案。
内容的提问来源于stack exchange,提问作者yangmillstheory
相关产品推荐
相关产品推荐

