C# 当哈希表缓存实现被清除时,我应该如何使其线程安全?

C# 当哈希表缓存实现被清除时,我应该如何使其线程安全?,c#,concurrency,locking,C#,Concurrency,Locking,我有一个类用于缓存对数据库资源的访问。它看起来像这样: //gets registered as a singleton class DataCacher<T> { IDictionary<string, T> items = GetDataFromDb(); //Get is called all the time from zillions of threads internal T Get(string key) {

我有一个类用于缓存对数据库资源的访问。它看起来像这样:

//gets registered as a singleton
class DataCacher<T>
{
    IDictionary<string, T> items = GetDataFromDb();

    //Get is called all the time from zillions of threads
    internal T Get(string key)
    {
         return items[key];
    }

    IDictionary<string, T> GetDataFromDb() { ...expensive slow SQL access... }

    //this gets called every 5 minutes
    internal void Reset()
    {
          items.Clear();
    }
}
//注册为单例
类数据缓存器
{
IDictionary items=GetDataFromDb();
//Get总是从无数线程中调用
内部T Get(字符串键)
{
返回项目[键];
}
IDictionary GetDataFromDb(){…昂贵的慢速SQL访问…}
//每5分钟打一次电话
内部无效重置()
{
items.Clear();
}
}
我对这段代码做了一些简化,但其要点是存在一个潜在的并发性问题,即在清除项时,如果调用Get,则可能会出错

现在我可以将锁块插入Get和Reset,但我担心Get上的锁会降低站点的性能,因为web应用程序中的每个请求线程都会多次调用Get

我想我可以用双重检查的锁来做一些事情,但是我怀疑有一种更干净的方法可以用比锁{}块更聪明的东西来做这件事。怎么办

edit:很抱歉,我之前没有明确说明这一点,但是我使用的items.Clear()实现实际上并不是一个直接的字典。它是一个围绕ResourceProvider的包装器,它要求字典实现在删除每个项时调用.ReleaseAllResources()。这意味着调用代码不希望针对正在处理中的旧版本运行。鉴于此,interlocated.Exchange方法是正确的吗?

我首先用一个
锁来测试它;锁在没有竞争时非常便宜。然而,一个更简单的方案是依赖于引用更新的原子性质:

public void Clear() {
    var tmp = GetDataFromDb(); // or new Dictionary<...> for an empty one
    items = tmp; // this is atomic; subsequent get/set will use this one
}
public void Clear(){
var tmp=GetDataFromDb();//或为空字典创建新字典
items=tmp;//这是原子的;后续的get/set将使用这个
}
您可能还希望将
设置为
易失性
字段,以确保它不会保存在寄存器中的任何位置

这仍然存在一个问题,即任何希望存在给定密钥的人可能会失望(通过异常),但这是一个单独的问题


更细粒度的选项可能是。

一个选项是完全替换IDictionary实例,而不是清除它。您可以使用类上的方法以线程安全的方式执行此操作。

查看数据库是否会告诉您哪些数据发生了更改。你可以用

  • 将更改写入历史记录表的触发器
  • (SqlServer和Oracle有这些功能,其他功能也必须具备)
因此,您不必根据计时器重新加载所有数据


没有做到这一点

我会让Clear方法通过调用GetDataFromDB()创建一个newIDictionary,然后在加载数据后,将“items”字段设置为指向新字典。(一旦没有线程访问旧字典,垃圾收集器将清理它。)

我想你不在乎这些线 在重新加载时获取“旧”结果 数据–(如果您这样做,您将 必须阻止锁上的所有螺纹- 痛!)


如果需要所有线程同时切换到新字典,则需要声明“items”字段为volatile,并在联锁类上使用Exchange方法。但是,在现实生活中,您不太可能需要它。

除非您有多个线程依赖于原子引用更新,否则不需要使用
联锁的
;get/set使用旧的还是新的并不重要,因此最好只更新字段。需要注意避免这些陷阱,如果(x.ContainsKey(y)){var ydata=x[y];…}
。一个只允许
TryGet`访问的实现不会有这个问题。只需使用ReaderWriterLockSlim——它与ReaderWriterLock具有相同的语义,但速度要快好几倍。ps:我讨厌答案显然不够广为人知,无法被每个人简单地表达出来。这是一个非常主流的问题,实际上只有一个正确的答案。