Go中的线程安全(Goroutine-safe)缓存

时间:2012-07-11 12:06:45

标签: caching interface thread-safety go lru

问题1

我正在为我的服务器构建/搜索RAM内存缓存层。它是一个简单的LRU缓存,需要处理并发请求(两者都是一组)。

我发现https://github.com/pmylund/go-cache声称是线程安全的。

就获取存储的界面而言,这是事实。但是如果多个goroutine请求相同的数据,它们都会检索指针(存储在接口中)到同一块内存。如果任何goroutine改变了数据,这将不再是非常安全的。

是否有解决此问题的缓存包?


问题1.1

如果问题1 的答案是,那么建议的解决方案是什么?
我看到两个选择:

备选方案1
解决方案:使用sync.Mutex将值存储在包装结构中,以便每个goroutine在读取/写入数据之前都需要锁定数据。
type cacheElement struct { value interface{}, lock sync.Mutex }
缺点:缓存变得不知道对数据所做的更改,甚至可能已将其从缓存中删除。一个goroutine也可能锁定其他人。

备选方案2
解决方案:制作数据的副本(假设数据本身不包含指针)
缺点:每次执行缓存Get时都会分配内存,更多的垃圾回收。


对于多部分问题,我们深表歉意。但你不必回答所有问题。如果您对问题1有一个很好的答案,那对我来说已经足够了!

2 个答案:

答案 0 :(得分:5)

备选方案2对我来说听起来不错,但请注意,您不必为每个cache.Get()复制数据。只要您的数据可以被视为不可变,您就可以同时使用多个读取器访问它。

如果您想要修改副本,则只需创建副本。这个习惯用法称为COW(写入时复制),在并发软件设计中很常见。它特别适用于具有高读/写比率的场景(就像缓存一样)。

因此,每当您想要修改缓存条目时,您基本上都必须:

  1. 创建旧缓存数据的副本(如果有)。
  2. 修改数据(在此步骤之后,数据应被视为不可变且不得再更改)
  3. 添加/替换缓存中的现有元素。您可以使用之前指出的go-cache库(基于锁定),也可以编写自己的无锁库,只需将指针原子交换到数据元素即可。
  4. 此时执行cache.Get操作的任何goroutine都将获得新数据。但是,现有的goroutine可能仍在读取旧数据。因此,您的程序可能同时在同一数据的许多不同版本上运行。但不要担心,只要所有goroutine都已完成访问旧数据,GC就会自动收集它。

答案 1 :(得分:3)

tux21b给出了一个很好的答案。我只想指出,您不必返回指向数据的指针。您可以将非指针值存储在缓存中,并且将传递将作为副本的值。然后你的Get和Set方法将是安全的,因为没有任何东西可以实际修改缓存内容。