在字典的键上使用锁定
我有一个Dictionary <string,someobject>
。
编辑:有人向我指出,我的榜样很糟糕。我的整个意图不是在循环中更新引用,而是根据需要更新/获取数据的不同线程来更新不同的值。我将循环更改为方法。
我需要一次更新一个字典中的项,而我想知道使用字典对象的.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(); } }
那会在法庭上受阻还是失败?我只希望字典中的每个值都被独立锁定,因此锁定(和更新)一个值不会锁定另一个值。我也知道锁定将保持很长一段时间,但数据将是无效的,直到完全更新。
解决方案
不,这行不通。
原因是字符串实习。这意味着:
string a = "Something"; string b = "Something";
都是同一个对象!因此,我们永远不要锁定字符串,因为如果程序的其他部分(例如,同一对象的另一个实例)也想锁定相同的字符串,则可能会在不需要它的地方意外地创建锁定争用;甚至可能陷入僵局。
不过,请随意使用非字符串来执行此操作。为了最清晰起见,我习惯总是创建一个单独的锁对象:
class Something { bool threadSafeBool = true; object threadSafeBoolLock = new object(); // Always lock this to use threadSafeBool }
我建议我们也这样做。使用每个矩阵单元的锁定对象创建一个Dictionary。然后,在需要时锁定这些对象。
PS。更改我们要遍历的集合并不是很好。对于大多数集合类型,它甚至会引发异常。尝试重构它,例如如果它总是恒定的,则遍历键列表,而不是对。
锁定在代码锁定之外可访问的对象上会带来很大的风险。如果有任何其他代码(在任何地方)锁定了该对象,那么我们可能会陷入一些难以调试的死锁中。还要注意,我们锁定的是对象,而不是引用,因此,如果我给我们提供字典,我可能仍会保留对键的引用并对其进行锁定,从而导致我们锁定同一对象。
如果我们完全封装了字典并自己生成了密钥(它们从未被传递过,那么我们可能会很安全)。
但是,请尽量坚持一条规则,以将锁定的对象的可见性限制为锁定代码本身。
这就是为什么我们看到以下内容:
public class Something { private readonly object lockObj = new object(); public SomethingReentrant() { lock(lockObj) // Line A { // ... } } }
而不是看到上面的A线被替换为
lock(this)
这样,一个单独的对象被锁定,可见性受到限制。
编辑Jon Skeet正确观察到上述lockObj应该是只读的。
在示例中,我们无法做我们想做的事!
我们将收到一个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()
我在那里看到了一些潜在的问题:
- 可以共享字符串,因此我们不必知道还有其他人可能由于其他原因锁定该关键对象
- 字符串可能无法共享:我们可能使用值" Key1"锁定一个字符串键,而其他代码段可能具有另一个字符串对象,该对象也包含字符" Key1"。对于字典,它们是相同的键,但就锁定而言,它们是不同的对象。
- 锁定不会阻止对值对象本身的更改,即
matrixElements [someKey] .ChangeAllYourContents()
注意:我假设在迭代过程中修改集合时的异常已修复
字典不是线程安全的集合,这意味着在没有外部同步的情况下修改和读取来自不同线程的集合是不安全的。 Hashtable对于(单写多读者)方案是(安全的)线程安全的,但是Dictionary具有不同的内部数据结构,并且不继承此保证。
这意味着我们在访问字典以从其他线程进行读取或者写入时无法修改字典,因为字典可能破坏了内部数据结构。锁定键并不能保护内部数据结构,因为在修改该键时,可能有人在另一个线程中读取字典中的其他键。即使我们可以保证所有键都是相同的对象(例如关于字符串实习的说法),但这也不能使我们感到安全。例子:
- 我们锁定密钥并开始修改字典
- 另一个线程尝试获取恰好落入与锁定对象相同的存储桶中的密钥的值。这不仅在两个对象的哈希码相同时,而且在hashcode%tableSize相同时更常见。
- 两个线程都访问相同的存储桶(具有相同hashcode%tableSize值的键的链接列表)
如果字典中没有这样的键,则第一个线程将开始修改列表,而第二个线程将可能读取未完成状态。
如果这样的键已经存在,则字典的实现细节仍可以修改数据结构,例如将最近访问的键移动到列表的开头,以加快检索速度。我们不能依靠实施细节。
当字典损坏时,有很多类似的情况。因此,我们必须具有外部同步对象(或者,如果不公开,则使用Dictionary本身),并在整个操作过程中将其锁定。如果在操作可能需要较长时间的情况下需要更多粒度的锁,则可以复制需要更新的键,对其进行迭代,在单键更新过程中锁定整个字典(不要忘记验证键是否仍然存在)并将其释放到让其他线程运行。
如果我没记错的话,最初的意图是锁定单个元素,而不是锁定整个字典(例如数据库中的表级锁定与行级锁定)
我们不能像这里解释的那样锁定字典的键。
我们可以做的是保留一个锁定对象的内部字典,该字典对应于实际的字典。因此,当我们要写入YourDictionary [Key1]时,首先要锁定InternalLocksDictionary [Key1],这样只有一个线程才能写入YourDictionary。
一个(不太干净)的例子可以在这里找到。