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

时间:2008-10-01 14:36:52

标签: c# .net thread-safety collections

我能够通过从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(...);
}

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

8 个答案:

答案 0 :(得分:204)

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

答案 1 :(得分:58)

尝试内部同步几乎肯定是不够的,因为它的抽象级别太低了。假设您使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方法将抛出异常,指示密钥已存在于字典中。您可能会问,如何使用线程安全的字典?好吧,因为每个操作都是线程安全的,两个操作的组合不是,因为另一个线程可以在您调用ContainsKeyAdd之间修改它。

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

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

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

  1. 使用普通Dictionary<TKey, TValue>并在外部同步,将复合操作封闭在其上,或

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

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

答案 2 :(得分:43)

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

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

答案 3 :(得分:6)

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

如果使用标准锁定表现不佳,那么Wintellect的Power Threading锁定集合可能非常有用。

答案 4 :(得分:4)

您正在描述的实现方法存在一些问题。

  1. 您不应该公开同步对象。这样做会打开一个消费者抓住物体并锁上它然后你敬酒。
  2. 您正在使用线程安全类实现非线程安全接口。恕我直言,这将花费你的路上
  3. 就个人而言,我发现实现线程安全类的最佳方法是通过不变性。它确实减少了线程安全可能遇到的问题。查看Eric Lippert's Blog了解更多详情。

答案 5 :(得分:2)

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

详细说明: 最终发生的事情是你的字典被锁定的时间比必要的时间长。

您的情况如下:

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

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

答案 6 :(得分: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);
    }

答案 7 :(得分:-5)