实现线程安全字典的最佳方法是什么?

GP.*_*GP. 108 .net c# collections thread-safety

我能够通过从IDictionary派生并定义一个私有的SyncRoot对象,在C#中实现一个线程安全的Dictionary:

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public object SyncRoot
    {
        get { return syncRoot; }
    } 

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
    }

    // more IDictionary members...
}
Run Code Online (Sandbox Code Playgroud)

然后,我在整个消费者(多个线程)中锁定此SyncRoot对象:

例:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}
Run Code Online (Sandbox Code Playgroud)

我能够使它工作,但这导致了一些丑陋的代码.我的问题是,是否有更好,更优雅的方式来实现线程安全的字典?

Hec*_*rea 205

命名支持并发的.NET 4.0类ConcurrentDictionary.

  • (请记住,另一个答案是在.NET 4.0(2010年发布)存在之前写的.) (27认同)
  • 请将此标记为响应,如果自己的.Net有解决方案,则不需要自定义的dictoniary (4认同)
  • 不幸的是,它不是一个无锁解决方案,因此它在 SQL Server CLR 安全程序集中没有用处。您需要类似此处描述的内容:http://www.cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf 或者可能是此实现:https://github.com/hackcraft/Ariadne (2认同)
  • 我知道它确实很老,但是需要注意的是,使用ConcurrentDictionary和Dictionary可能会导致严重的性能损失。这很可能是昂贵的上下文切换的结果,因此请确保在使用字典之前需要一个线程安全的字典。 (2认同)

Gre*_*ech 59

试图在内部进行同步几乎肯定是不够的,因为它的抽象级别太低了.假设您按照以下方式单独使用AddContainsKey操作线程安全:

public void Add(TKey key, TValue value)
{
    lock (this.syncRoot)
    {
        this.innerDictionary.Add(key, value);
    }
}

public bool ContainsKey(TKey key)
{
    lock (this.syncRoot)
    {
        return this.innerDictionary.ContainsKey(key);
    }
}
Run Code Online (Sandbox Code Playgroud)

那么当你从多个线程中调用这个所谓的线程安全的代码时会发生什么?它总能正常工作吗?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}
Run Code Online (Sandbox Code Playgroud)

简单回答是不.在某些时候,该Add方法将抛出一个异常,表明该密钥已经存在于字典中.您可能会问,如何使用线程安全字典?好吧,因为每个操作都是线程安全的,两个操作的组合不是,因为另一个线程可以在你的调用ContainsKey和之间修改它Add.

这意味着要正确地编写这种类型的场景,你需要在字典之外锁定,例如

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是现在,当你需要编写外部锁定代码时,你会混淆内部和外部同步,这总是会导致诸如代码不清晰和死锁等问题.所以最终你可能会更好:

  1. 使用法线Dictionary<TKey, TValue>并在外部同步,将复合操作包含在其上,或

  2. 编写一个具有不同接口(即不是IDictionary<T>)的新线程安全包装器,它结合了诸如AddIfNotContained方法之类的操作,因此您永远不需要组合它的操作.

(我倾向于自己选择#1)

  • 值得指出的是,.NET 4.0将包含一大堆线程安全容器,如集合和字典,它们与标准集合具有不同的接口(即它们正在为您执行上面的选项2). (10认同)

fry*_*bob 43

正如Peter所说,您可以封装类中的所有线程安全性.您需要小心处理您公开或添加的任何事件,确保它们在任何锁定之外被调用.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    public void Add(TKey key, TValue value)
    {
        lock (syncRoot)
        {
            d.Add(key, value);
        }
        OnItemAdded(EventArgs.Empty);
    }

    public event EventHandler ItemAdded;

    protected virtual void OnItemAdded(EventArgs e)
    {
        EventHandler handler = ItemAdded;
        if (handler != null)
            handler(this, e);
    }

    // more IDictionary members...
}
Run Code Online (Sandbox Code Playgroud)

编辑: MSDN文档指出枚举本质上不是线程安全的.这可能是在类外暴露同步对象的一个​​原因.另一种方法是提供一些方法来对所有成员执行操作并锁定成员的枚举.这个问题是你不知道传递给该函数的动作是否会调用你字典的某个成员(这会导致死锁).公开同步对象允许消费者做出这些决定,而不会隐藏类中的死锁.

  • 这也不是一个固有的线程安全类,因为字典操作往往是粒度的.沿着if(dict.Contains(what)){dict.Remove(what); dict.Add(无论如何,newval); 肯定是竞争状态等待发生. (12认同)
  • 我的字典不太大,我认为这确实有用。我所做的是创建一个名为 CopyForEnum() 的新公共方法,它返回带有私有字典副本的字典的新实例。然后调用此方法进行枚举,并删除 SyncRoot。谢谢! (2认同)

Jon*_*ebb 6

您不应通过属性发布私有锁对象.锁定对象应该私有存在,其唯一目的是充当会合点.

如果使用标准锁定性能很差,那么Wintellect的Power Threading锁定系列非常有用.


Jar*_*Par 5

您描述的实现方法存在几个问题。

  1. 您不应该公开同步对象。这样做将使消费者容易抓住物品并对其进行锁定,然后烤面包。
  2. 您正在使用线程安全类实现非线程安全接口。恕我直言,这将花费您一路走来

就个人而言,我发现实现线程安全类的最佳方法是通过不变性。它确实减少了线程安全可能遇到的问题。查看Eric Lippert的博客以获取更多详细信息。