110

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

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(...);
}

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

4

8 回答 8

207

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

于 2010-09-13T19:11:21.937 回答
63

尝试在内部进行同步几乎肯定是不够的,因为它的抽象级别太低了。假设您使AddandContainsKey操作单独成为线程安全的,如下所示:

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方法将抛出一个异常,指示该键已存在于字典中。你可能会问,这怎么可能是线程安全的字典?好吧,因为每个操作都是线程安全的,所以两个操作的组合不是,因为另一个线程可以在您调用ContainsKeyand之间修改它Add

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

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

但是现在,由于您必须编写外部锁定代码,因此您将内部和外部同步混为一谈,这总是会导致代码不清晰和死锁等问题。所以最终你可能会更好:

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

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

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

于 2008-12-30T00:07:59.377 回答
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 文档指出枚举本质上不是线程安全的。这可能是在类之外公开同步对象的原因之一。另一种方法是提供一些方法来对所有成员执行操作并锁定成员的枚举。这样做的问题是您不知道传递给该函数的操作是否调用了字典的某些成员(这将导致死锁)。公开同步对象允许消费者做出这些决定,并且不会隐藏类中的死锁。

于 2008-10-01T14:49:48.007 回答
6

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

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

于 2008-10-01T14:51:53.757 回答
5

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

  1. 你永远不应该公开你的同步对象。这样做会向消费者敞开心扉,抓住物体并锁定它,然后你就完蛋了。
  2. 您正在使用线程安全类实现非线程安全接口。恕我直言,这将使您付出代价

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

于 2008-10-01T15:13:32.597 回答
3

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

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

在您的情况下会发生以下情况:

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

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

于 2008-10-01T14:42:32.220 回答
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);
    }
于 2012-11-16T15:27:07.323 回答
-6

集合和同步

于 2008-10-01T14:52:50.587 回答