You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通道并发保证:并发安全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或数据不一致问题。
  • 通道作为同步广播信号:
    1. 第一个处理未缓存key的goroutine,会先创建对应key的done通道,释放锁后执行计算;
    2. 计算完成后,将结果写入memo,再关闭done通道——通道关闭是一个广播信号,所有阻塞在这个通道上的goroutine都会被唤醒;
    3. 后续请求该key的goroutine,发现done通道存在时,会直接阻塞在通道上,直到收到完成信号,再从memo读取已计算好的结果。

实现时需要注意的细节

  • 锁的粒度优化:用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:46:40