8

我有一个Dictionary<string, someobject>.

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

我需要更新字典中的项目 - 一次一个键,我想知道在我的 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();
    }
}

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

4

7 回答 7

11

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

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

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

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

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

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

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

  lock(this)

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

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

于 2008-10-01T13:23:17.357 回答
10

不,这行不通。

原因是字符串实习。这意味着:

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

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

不过,请随意使用非字符串来执行此操作。为了清楚起见,我将始终创建一个单独的锁定对象作为个人习惯:

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

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

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

于 2008-10-01T13:18:31.470 回答
3

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

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

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

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

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

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

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

于 2008-10-01T16:02:13.090 回答
2

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

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

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

一个(不太干净)的例子可以在这里找到

于 2009-02-11T07:45:43.987 回答
1

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

 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;
        }
    }
于 2016-04-14T11:11:27.520 回答
0

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

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

下面举个例子来证明:

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()
于 2008-10-01T13:24:58.037 回答
0

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

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