【问题标题】:What's the best way of implementing a thread-safe Dictionary?实现线程安全字典的最佳方法是什么?
【发布时间】:2008-10-01 14:36:52
【问题描述】:

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

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

【问题讨论】:

  • 你觉得它有什么难看的地方?
  • 我认为他指的是他在 SharedDictionary 类的消费者中的整个代码中拥有的所有锁定语句——他每次访问方法时都会锁定 calling 代码一个 SharedDictionary 对象。
  • 不要使用 Add 方法,而是尝试通过分配值 ex- m_MySharedDictionary["key1"]="item1",这是线程安全的。

标签: c# .net thread-safety collections


【解决方案1】:

尝试在内部进行同步几乎肯定是不够的,因为它的抽象级别太低了。假设您将 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&lt;TKey, TValue&gt; 并在外部同步,将复合操作封装在其上,或者

  2. 编写一个新的线程安全包装器,它具有不同的接口(即不是IDictionary&lt;T&gt;),它结合了AddIfNotContained 方法等操作,因此您永远不需要结合它的操作。

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

【讨论】:

  • 值得指出的是,.NET 4.0 将包括一大堆线程安全的容器,例如集合和字典,它们与标准集合有不同的接口(即他们正在执行上面的选项 2给你)。
  • 同样值得注意的是,如果设计一个合适的枚举器让底层类知道它何时被释放(方法如果存在未处理的枚举器时想要编写字典,则应将字典替换为副本)。
【解决方案2】:

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

【讨论】:

  • 请将此标记为响应,如果自己的 .Net 有解决方案,则不需要自定义词典
  • (请记住,另一个答案早在 .NET 4.0(2010 年发布)出现之前就已经写好了。)
  • 不幸的是,它不是无锁解决方案,因此在 SQL Server CLR 安全程序集中没有用处。你需要像这里描述的东西:cse.chalmers.se/~tsigas/papers/Lock-Free_Dictionary.pdf 或者这个实现:github.com/hackcraft/Ariadne
  • 真的很老,我知道,但重要的是要注意,使用 ConcurrentDictionary 与 Dictionary 可能会导致显着的性能损失。这很可能是代价高昂的上下文切换的结果,因此请确保在使用之前需要一个线程安全的字典。
  • ConcurrentDictionary 在某些情况下可能会非常慢。根据您的用例,一些替代的ThreadSafeDictionary 实现可能会更好(免责声明:由我编写)。有关比较和推荐的用例,请参见页面底部。 (NuGet)
【解决方案3】:

你描述的实现方法有几个问题。

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

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

【讨论】:

    【解决方案4】:

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

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

    您的情况如下:

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

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

    【讨论】:

    • 不正确...如果您一个接一个地执行两个内部线程安全的操作,这并不意味着整个代码块是线程安全的。例如: if(!myDict.ContainsKey(someKey)) { myDict.Add(someKey, someValue); } 不会是线程安全的,即使它 ContainsKey 和 Add 是线程安全的
    • 您的观点是正确的,但与我的回答和问题无关。如果你看这个问题,它没有谈到调用 ContainsKey,我的回答也没有。我的回答是指获取 SyncRoot 上的锁定,如原始问题的示例中所示。在 lock 语句的上下文中,一个或多个线程安全操作确实可以安全执行。
    • 我想如果他所做的只是添加到字典中,但由于他有“// 更多 IDictionary 成员......”,我假设在某些时候他也会想要阅读从字典中返回数据。如果是这种情况,那么就需要一些外部可访问的锁定机制。不管是字典本身中的 SyncRoot 还是其他单独用于锁定的对象都没有关系,但是如果没有这样的方案,整个代码将不是线程安全的。
    • 外部锁定机制如他在问题示例中所示:lock (m_MySharedDictionary.SyncRoot) { m_MySharedDictionary.Add(...); } -- 执行以下操作会非常安全: lock (m_MySharedDictionary.SyncRoot) { if (!m_MySharedDictionary.Contains(...)) { m_MySharedDictionary.Add(...); } } 换句话说,外部锁定机制是对公共属性 SyncRoot 进行操作的锁定语句。
    【解决方案5】:

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

    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:枚举实际上是我公开同步对象的唯一原因。按照惯例,只有在枚举集合时,我才会对该对象执行锁定。
    • 如果您的字典不是太大,您可以枚举副本并将其内置到类中。
    • 我的字典不是太大,我认为这可以解决问题。我所做的是创建一个名为 CopyForEnum() 的新公共方法,它返回带有私有字典副本的 Dictionary 的新实例。然后调用此方法进行枚举,并删除了 SyncRoot。谢谢!
    • 这也不是一个固有的线程安全类,因为字典操作往往是细粒度的。沿着 if (dict.Contains(whatever)) { dict.Remove(whatever); dict.Add(无论如何,newval); } 肯定是等待发生的竞争条件。
    【解决方案6】:

    【讨论】:

    • -1 我投了反对票,因为a)它只是一个没有解释的链接,b)它只是一个没有解释的链接!
    【解决方案7】:

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

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

    【讨论】:

      【解决方案8】:

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

      例子

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

      【讨论】:

        猜你喜欢
        • 2010-10-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-04-06
        • 1970-01-01
        相关资源
        最近更新 更多