【问题标题】:Using lock on the key of a Dictionary<string, object>在 Dictionary<string, object> 的键上使用锁定
【发布时间】:2008-10-01 13:12:16
【问题描述】:

我有一个Dictionary&lt;string, someobject&gt;

编辑:有人向我指出,我的例子很糟糕。我的整个意图不是更新循环中的引用,而是根据不同的线程更新不同的值需要更新/获取数据。我将循环更改为方法。

我需要更新字典中的项目 - 一次一个键,我想知道在我的 Dictionary 对象的 .key 值上使用锁定是否有任何问题?

private static Dictionary<string, MatrixElement> matrixElements = new Dictionary<string, MatrixElement>();

//Pseudo-code

public static void UpdateValue(string key)
{
    KeyValuePair<string, MatrixElement> keyValuePair = matrixElements[key];
    lock (keyValuePair.Key)
    {
        keyValuePair.Value  = SomeMeanMethod();
    }
}

这会在法庭上成立还是失败?我只希望字典中的每个值都被独立锁定,因此锁定(和更新)一个值不会锁定其他值。另外我知道锁定将持续很长时间 - 但数据在完全更新之前将无效。

【问题讨论】:

  • 不是一个答案,但你知道如果你已经有了这对,你可以使用 pair.Value 而不是在字典上再次调用 operator[]?
  • 其实我试过了,但是值是只读的还是我的IDE告诉我的?
  • 你不能修改 foreach 元素
  • 你想避免的并发到底在哪里?如果您在多个线程中运行循环,第一个线程将锁定第一个键,所有其他线程将在点击第一个键时阻塞。所以我不明白你想要解决的问题。你能澄清你在这方面的问题吗?
  • TToni:你说得对,但我这里的例子是简化的——它们不会在循环中更新——这只是我做的一个坏例子——每个线程可能会更新不同的值——或者相同我想避免。

标签: c# dictionary locking


【解决方案1】:

锁定在代码锁定之外可访问的对象是一个很大的风险。如果任何其他代码(任何地方)曾经锁定该对象,您可能会陷入一些难以调试的死锁。另请注意,您锁定的是对象,而不是引用,所以如果我给你一本字典,我可能仍然持有对键的引用并锁定它们——导致我们锁定同一个对象。

如果您完全封装了字典,并自己生成密钥(它们从未传入,那么您可能是安全的。

但是,请尽量遵守一条规则 - 尽可能将您锁定的对象的可见性限制为锁定代码本身。

这就是你看到这个的原因:

public class Something
{
  private readonly object lockObj = new object();

  public SomethingReentrant()
  {
    lock(lockObj)    // Line A
    {
      // ...
     }
   }
}

而不是看到上面的A行替换为

  lock(this)

这样,一个单独的对象被锁定,可见性受到限制。

编辑 Jon Skeet 正确观察到上面的 lockObj 应该是只读的。

【讨论】:

  • 我要补充一点,lockObj 也应该几乎总是被声明为只读的。
【解决方案2】:

不,这行不通。

原因是string interning。这意味着:

string a = "Something";
string b = "Something";

都是同一个对象!因此,您永远不应该锁定字符串,因为如果程序的其他部分(例如,同一对象的另一个实例)也想锁定相同的字符串,您可能会意外地在不需要它的地方创建锁定争用;甚至可能陷入僵局。

尽管如此,您可以随意使用非字符串。为了清楚起见,我将始终创建一个单独的锁定对象作为个人习惯:

class Something
{
    bool threadSafeBool = true;
    object threadSafeBoolLock = new object(); // Always lock this to use threadSafeBool
}

我建议你也这样做。为每个矩阵单元创建一个带有锁对象的字典。然后,在需要时锁定这些对象。

PS。更改您正在迭代的集合被认为不是很好。它甚至会抛出大多数集合类型的异常。尝试重构这个 - 例如遍历一个键列表,如果它始终是常量,而不是对。

【讨论】:

  • 您还可以禁用字符串实习 - 我相信所有 .NET 框架程序集都会这样做。
  • 字典的“键”可以是任何对象,而不仅仅是字符串!如果键是整数,这将不适用!
  • 重要的是要注意只有字符串字面量是隐式的。必须将 string.Intern() 用于其他字符串。我完全同意你的观点,即字符串对于这种类型的事情是不安全的,因为任何给定的字符串都可能被保留,这可能会导致锁定时出现微妙的问题。
【解决方案3】:

注意:我假设在迭代期间修改集合时的异常已经修复

字典不是线程安全的集合,这意味着在没有外部同步的情况下从不同的线程修改和读取集合是安全的。 Hashtable 是(曾经?)线程安全的一个作者多读者场景,但 Dictionary 具有不同的内部数据结构并且不继承此保证。

这意味着当您从其他线程访问字典以进行读取或写入时,您无法修改字典,它只会破坏内部数据结构。锁定密钥并不能保护内部数据结构,因为当您修改该密钥时,有人可能会在另一个线程中读取您字典的不同密钥。即使你可以保证你所有的键都是相同的对象(比如关于字符串实习的说法),这并不能让你安全。示例:

  1. 您锁定密钥并开始修改字典
  2. 另一个线程尝试获取键的值,该键恰好落入与锁定的存储桶相同的存储桶中。这不仅在两个对象的 hashcode 相同时出现,而且在 hashcode%tableSize 相同时更常见。
  3. 两个线程都在访问同一个存储桶(具有相同 hashcode%tableSize 值的键的链接列表)

如果字典中没有这样的键,第一个线程将开始修改列表,第二个线程可能会读取不完整的状态。

如果这样的键已经存在,字典的实现细节仍然可以修改数据结构,例如将最近访问的键移动到列表的头部以便更快地检索。您不能依赖实施细节。

在很多情况下,您的字典都会损坏。因此,您必须拥有外部同步对象(或使用 Dictionary 本身,如果它不公开)并在整个操作期间锁定它。如果您在操作可能需要很长时间时需要更细粒度的锁,您可以复制需要更新的密钥,对其进行迭代,在单个密钥更新期间锁定整个字典(不要忘记验证密钥是否仍然存在)并将其释放到让其他线程运行。

【讨论】:

    【解决方案4】:

    如果我没记错的话,最初的意图是锁定单个元素,而不是锁定整个字典(就像数据库中的表级锁定与行级锁定)

    您不能像这里解释的那样锁定字典的键。

    您可以做的是保留与实际字典相对应的锁定对象的内部字典。因此,当您想写入 YourDictionary[Key1] 时,您将首先锁定 InternalLocksDictionary[Key1] - 这样只有一个线程会写入 YourDictionary。

    一个(不太干净的)例子可以是found here

    【讨论】:

      【解决方案5】:

      刚遇到这个,想分享一些我几年前写的代码,我需要一个关键的字典

       using (var lockObject = new Lock(hashedCacheID))
       {
          var lockedKey = lockObject.GetLock();
          //now do something with the dictionary
       }
      

      锁类

      class Lock : IDisposable
          {
              private static readonly Dictionary<string, string> Lockedkeys = new Dictionary<string, string>();
      
              private static readonly object CritialLock = new object();
      
              private readonly string _key;
              private bool _isLocked;
      
              public Lock(string key)
              {
                  _key = key;
      
                  lock (CritialLock)
                  {
                      //if the dictionary doesnt contain the key add it
                      if (!Lockedkeys.ContainsKey(key))
                      {
                          Lockedkeys.Add(key, String.Copy(key)); //enusre that the two objects have different references
                      }
                  }
              }
      
              public string GetLock()
              {
                  var key = Lockedkeys[_key];
      
                  if (!_isLocked)
                  {
                      Monitor.Enter(key);
                  }
                  _isLocked = true;
      
                  return key;
              }
      
              public void Dispose()
              {
                  var key = Lockedkeys[_key];
      
                  if (_isLocked)
                  {
                      Monitor.Exit(key);
                  }
                  _isLocked = false;
              }
          }
      

      【讨论】:

        【解决方案6】:

        在你的例子中,你不能做你想做的事!

        你会得到一个System.InvalidOperationException,其中包含Collection was modified的消息;枚举操作可能无法执行。

        举个例子来证明:

        using System.Collections.Generic;
        using System;
        
        public class Test
        {
            private Int32 age = 42;
        
            static public void Main()
            {
               (new Test()).TestMethod();
            }
        
            public void TestMethod()
            {
                Dictionary<Int32, string> myDict = new Dictionary<Int32, string>();
        
                myDict[age] = age.ToString();
        
                foreach(KeyValuePair<Int32, string> pair in myDict)
                {
                    Console.WriteLine("{0} : {1}", pair.Key, pair.Value);
                    ++age;
                    Console.WriteLine("{0} : {1}", pair.Key, pair.Value);
                    myDict[pair.Key] = "new";
                    Console.WriteLine("Changed!");
                }
            }   
        }
        

        输出将是:

        42 : 42
        42 : 42
        
        Unhandled Exception: System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
           at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)
           at System.Collections.Generic.Dictionary`2.Enumerator.MoveNext()
           at Test.TestMethod()
           at Test.Main()
        

        【讨论】:

        • 我不想锁定整个字典 - 只锁定 keypairvalue - 所以没有两个进程会同时更新 keypairvalue 中的相同值。
        • 我没有改变密钥 - 只有值,所以它不应该是一个问题
        【解决方案7】:

        我可以看到一些潜在的问题:

        1. 字符串可以共享,因此您不一定知道还有谁可能出于其他原因锁定该密钥对象
        2. 字符串可能被共享:您可能锁定了一个值为“Key1”的字符串键,而其他一些代码可能有一个不同的字符串对象,其中也包含字符“Key1” .对于字典来说,它们是相同的键,但就锁定而言,它们是不同的对象。
        3. 锁定不会阻止值对象本身的更改,即matrixElements[someKey].ChangeAllYourContents()

        【讨论】:

          猜你喜欢
          • 2012-03-01
          • 1970-01-01
          • 1970-01-01
          • 2013-05-15
          • 2014-10-22
          • 2012-05-13
          • 1970-01-01
          • 2015-04-12
          • 1970-01-01
          相关资源
          最近更新 更多