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


109

通过从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...
}

然后,我在整个使用者(多个线程)上锁定此SyncRoot对象:

例:

lock (m_MySharedDictionary.SyncRoot)
{
    m_MySharedDictionary.Add(...);
}

我能够使其工作,但这导致了一些难看的代码。我的问题是,是否有更好,更优雅的方法来实现线程安全字典?


3
您发现它有什么丑陋的地方?
Asaf R

1
我认为他是指他在SharedDictionary类的使用者中整个代码中拥有的所有lock语句- 每次访问SharedDictionary对象上的方法时,他都会锁定调用代码。
彼得·迈耶

而不是使用Add方法尝试通过分配值exm_MySharedDictionary [“ key1”] =“ item1”来做到这一点,这是线程安全的。
testuser 2012年

Answers:


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...
}

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


@fryguybob:实际上,枚举是我公开同步对象的唯一原因。按照惯例,仅当我枚举集合时,才对该对象执行锁定。
GP。

1
如果字典不是太大,您可以列举一个副本并将其内置到类中。
fryguybob

2
我的字典不太大,我认为可以解决问题。我所做的是制作一个名为CopyForEnum()的新公共方法,该方法返回包含私有字典副本的Dictionary的新实例。然后调用此方法进行枚举,并删除了SyncRoot。谢谢!
GP。

12
这也不是固有的线程安全类,因为字典操作趋向于精细。遵循if(dict.Contains(whatever)){dict.Remove(whatever); dict.Add(无论是newval); }肯定是一个竞争条件正在等待发生。
底座

207

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


4
请注明此作为回应,你不需要定制dictoniary如果自己的.NET有一个解决方案
阿尔贝托·莱昂

27
(请记住,其他答案早于.NET 4.0(2010年发布)就已经写了。)
Jeppe Stig Nielsen

1
不幸的是,它不是无锁解决方案,因此在SQL Server CLR安全程序集中没有用。您需要类似此处所述的内容:cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf或以下实现:github.com/hackcraft/Ariadne
Triynko 2013年

2
我知道它确实很旧,但是需要注意的是,使用ConcurrentDictionary和Dictionary可能会导致严重的性能损失。这很可能是昂贵的上下文切换导致的,因此请确保在使用字典之前需要一个线程安全的字典。
2016年

60

在内部尝试进行同步几乎肯定是不够的,因为它的抽象级别太低。假设您将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);
    }
}

那么,当您从多个线程中调用这个所谓的线程安全代码时,会发生什么?会一直正常吗?

if (!mySafeDictionary.ContainsKey(someKey))
{
    mySafeDictionary.Add(someKey, someValue);
}

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

这意味着写这种类型的场景正确,您需要一个锁外面的字典,如

lock (mySafeDictionary)
{
    if (!mySafeDictionary.ContainsKey(someKey))
    {
        mySafeDictionary.Add(someKey, someValue);
    }
}

但是现在,由于您不得不编写外部锁定代码,因此您正在混淆内部和外部同步,这总是会导致代码不清楚和死锁等问题。因此,最终您可能会更好:

  1. 使用一个法线Dictionary<TKey, TValue>并在外部进行同步,将其上的复合操作括起来,或者

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

(我自己倾向于和#1一起去)


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

1
还值得注意的是,如果一个人设计了一个合适的枚举器,该枚举器可以让底层的类知道何时被处置,那么即使是粗略的锁定所提供的粒度对于单写多读器方法也通常就足够了。存在未使用的枚举数应使用副本替换字典)。
2012年

6

您不应该通过属性发布私有锁对象。锁定对象应仅作为一个集合点而私下存在。

如果使用标准锁证明性能很差,则Wintellect的Power Threading锁集合可能非常有用。


5

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

  1. 您不应该公开同步对象。这样做将使您对抓住物品并锁定它的消费者敞开大门,然后您就敬酒了。
  2. 您正在使用线程安全类实现非线程安全接口。恕我直言,这将花费您一路走来

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


3

您不需要在使用者对象中锁定SyncRoot属性。您在字典方法中拥有的锁就足够了。

详细说明: 最终发生的事情是字典被锁定的时间超出了必要的时间。

您的情况如下:

假设线程A 在调用m_mySharedDictionary.Add 之前获得了SyncRoot的锁定。然后,线程B尝试获取该锁,但被锁定。实际上,所有其他线程均被阻止。允许线程A调用Add方法。在Add方法中的lock语句中,允许线程A再次获取该锁,因为它已经拥有该锁。在方法内退出锁上下文,然后在方法外退出时,线程A已释放所有锁,从而允许其他线程继续。

您可以简单地允许任何使用者调用Add方法,因为SharedDictionary类Add方法中的lock语句将具有相同的效果。此时,您具有冗余锁定。如果必须对必须保证连续发生的字典对象执行两项操作,则仅在其中一种字典方法之外锁定SyncRoot。


1
不正确...如果您在内部执行线程安全的两个操作之后,并不意味着整个代码块都是线程安全的。例如:if(!myDict.ContainsKey(someKey)){myDict.Add(someKey,someValue); }将不是线程安全的,即使它ContainsKey和Add也都是线程安全的
Tim

您的观点是正确的,但与我的回答和问题无关。如果您查看问题,它不会谈论调用ContainsKey,也不会回答我。我的答案是指获取SyncRoot的锁,该锁已显示在原始问题的示例中。在lock语句的上下文中,一个或多个线程安全操作确实可以安全地执行。
彼得·迈耶

我想他是否正在做的只是添加到字典中,但是由于他有“ //更多IDictionary成员...”,因此我认为在某个时候他也将要从字典中读取数据。如果是这种情况,则需要某种外部可访问的锁定机制。无论是字典本身中的SyncRoot还是仅用于锁定的另一个对象都没有关系,但是如果没有这种方案,则整个代码将不是线程安全的。
蒂姆(Tim)

外部锁定机制如他在问题中的示例所示:锁定(m_MySharedDictionary.SyncRoot){m_MySharedDictionary.Add(...); }-执行以下操作绝对是安全的:lock(m_MySharedDictionary.SyncRoot){如果(!m_MySharedDictionary.Contains(...)){m_MySharedDictionary.Add(...); 换句话说,外部锁定机制是对公共属性SyncRoot进行操作的lock语句。
彼得·迈耶

0

只是想到为什么不重新创建字典?如果读取是大量写入,则锁定将同步所有请求。

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    private string Fetch(string key)
    {
        lock (Lock)
        {
            string returnValue;
            if (_dict.TryGetValue(key, out returnValue))
                return returnValue;

            returnValue = "find the new value";
            _dict = new Dictionary<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.