在C#中双重检查锁定

时间:2012-11-22 15:50:44

标签: c# .net multithreading double-checked-locking

想法是从

获取一个值(“数据”)
  • 变量
  • 如果不存在,则从文件
  • 如果不存在,则从服务器
public static string LoadData(int id)
{
    if (_isDataLoaded[id - 1])
    {
      return _data[id - 1];
    }

    if (File.Exists(_fileName))
    {
        bool dataExists;
        string cachedData;
        if (GetCachedConstant(id, out cachedData)) //read from a file
        {
            lock (_locker)
            {
                _isDataLoaded[id - 1] = true;
                _data[id - 1] = cachedData;
            }

            return _data[id - 1];
        }
    }

    string remoteData = GetFromServer(id);
    lock (_locker)
    {
        CacheToFile(id, remoteData); //write to a file
        _data[id - 1] = remoteData;
        _isDataLoaded[id - 1] = true;
    }

    return _data[id - 1];
}

许多线程都使用此代码。虽然它似乎是线程安全的,但事实并非如此。因此,我测试了它,这给了我这个想法并让我确定。在写作之前应该对现有_data进行双重检查。例如,它应该像通常在模式Singleton中使用的双重检查。

有人请让我了解如何在此实施?

修改

string[] _data;
bool[] _isDataLoaded.

EDIT2

上面的代码可能与.NET< 4.0,所以不允许在那里使用Lasy。我现在唯一的问题是,如果我使用双重检查锁定,我应该使用volatile吗?

volatile string[] _data;
volatile bool[] _isDataLoaded.

6 个答案:

答案 0 :(得分:3)

我可以看到两个明显的线程问题来源

  • _data_isDataLoaded的类型是什么? 如果这些类型不是线程安全的,则代码 arrays are only thread safe if no two threads access the same element也不是,这可能发生在上面的代码中。请尝试使用ConcurrentDictionary代替。
  • 根据GetCachedConstantCacheToFile的确切实现,存在潜在的竞争条件,即线程A可以开始将缓存数据写入文件,从而导致线程B沿File.Exists路径向下移动实际上文件只包含部分写入的数据

我猜测罪魁祸首是这两个中的第一个 - 可能还有其他问题,但除非这些类型是线程安全的,否则没有多少双重检查锁定会保存你,除非你同步访问这些对象。

答案 1 :(得分:2)

我将_data替换为Lazy<string>[],其中从文件和网络中提取的内容发生在您传入的代理中。

因此在静态构造函数中执行以下操作:

_data=new Lazy<string>[maxId+1];
for(int i=0;i<_data.Length;i++)
{
  _data[i]=new Lazy<string>(()=>fetchData(i), LazyThreadSafetyMode.ExecutionAndPublication);
}

然后简单地获取值_data[i].Value


如果确实想要双重检查锁定,它基本上是这样的:

if (!_isDataLoaded[id - 1])
{
    lock(locker)
    {
        if(!_isDataLoaded[id - 1])
        {
            ...
            _data[id - 1] = ...;
            _isDataLoaded[id - 1] = true;
        }
    }
}
return _data[id - 1];

这段代码的问题在于很难弄清楚它是否真的有效。这取决于您运行的平台的内存模型。 AFAIK .net 2.0内存模型保证了这一点,但ECMA CLR模型和java模型没有。内存模型问题非常微妙,容易出错。所以我强烈建议不要使用这种模式。

答案 2 :(得分:1)

为什么不锁定方法的开头。这可以确保您的数据(缓存)始终处于有效/一致状态。

答案 3 :(得分:1)

如何做到这一点,保持一个锁加载/持久化数据到缓存

public static string LoadData(int id)
{
    if (_isDataLoaded[id - 1])
      return _data[id - 1];

    lock (_locker)
    {
        if (_isDataLoaded[id - 1])
          return _data[id - 1];

        if (File.Exists(_fileName))
        {
            bool dataExists;
            string cachedData;
            if (GetCachedConstant(id, out cachedData)) //read from a file
            {
                _data[id - 1] = cachedData;
                _isDataLoaded[id - 1] = true;
                return _data[id - 1];
            }
        }

        string remoteData = GetFromServer(id);
        CacheToFile(id, remoteData); //write to a file
        _data[id - 1] = remoteData;
        _isDataLoaded[id - 1] = true;
        return _data[id - 1];
    }
}

答案 4 :(得分:1)

    lock (_locker)
    {
        _isDataLoaded[id - 1] = true;
        _data[id - 1] = cachedData;
    }

此处_isDataLoaded设置在_data之前,因此有人在_isDataLoaded初始化之前会看到_data并从_data读取。

但逆转它并不能解决问题。不能保证另一个线程会以相同的顺序看到分配,因为读者不使用任何锁定或内存障碍。

检查维基百科有关使用C#执行此操作的正确方法。 http://en.wikipedia.org/wiki/Double-checked_locking

答案 5 :(得分:0)

双重检查锁定背后的想法就是它的名字。你在lock之前检查你的状况(比如在你的代码中),但是再次在lock块内检查你的情况,以确保另一个线程没有改变状态(即条件的结果)线程在(成功)检查和lock语句之间。