异常可能是由于在迭代过程中通过IEnumerator
. 您可以使用几种技术来维护线程安全。我将按难度顺序介绍它们。
锁定一切
到目前为止,这是获取线程安全数据结构的最简单和最简单的方法。当读取和写入操作的数量相等时,此模式效果很好。
LinkedList<object> collection = new LinkedList<object>();
void Write()
{
lock (collection)
{
collection.AddLast(GetSomeObject());
}
}
void Read()
{
lock (collection)
{
foreach (object item in collection)
{
DoSomething(item);
}
}
}
复制阅读模式
这是一个稍微复杂的模式。您会注意到数据结构的副本是在阅读之前制作的。当读取操作的数量与写入的数量相比很少并且复制的惩罚相对较小时,这种模式很有效。
LinkedList<object> collection = new LinkedList<object>();
void Write()
{
lock (collection)
{
collection.AddLast(GetSomeObject());
}
}
void Read()
{
LinkedList<object> copy;
lock (collection)
{
copy = new LinkedList<object>(collection);
}
foreach (object item in copy)
{
DoSomething(item);
}
}
复制-修改-交换模式
最后,我们有了最复杂和最容易出错的模式。我实际上不建议使用这种模式,除非你真的知道你在做什么。与我在下面的任何偏差都可能导致问题。这很容易搞砸。事实上,我过去也无意中把这个搞砸了。您会注意到数据结构的副本是在所有修改之前制作的。然后修改副本,最后将原始引用替换为新实例。基本上我们总是把collection
它当作不可变的。当写入操作的数量与读取的数量相比很少并且复制的惩罚相对较小时,这种模式很有效。
object lockobj = new object();
volatile LinkedList<object> collection = new LinkedList<object>();
void Write()
{
lock (lockobj)
{
var copy = new LinkedList<object>(collection);
copy.AddLast(GetSomeObject());
collection = copy;
}
}
void Read()
{
LinkedList<object> local = collection;
foreach (object item in local)
{
DoSomething(item);
}
}
更新:
所以我在评论区提出了两个问题:
- 为什么
lock(lockobj)
而不是lock(collection)
在写端?
- 为什么
local = collection
在阅读方面?
关于第一个问题,考虑 C# 编译器如何扩展lock
.
void Write()
{
bool acquired = false;
object temp = lockobj;
try
{
Monitor.Enter(temp, ref acquired);
var copy = new LinkedList<object>(collection);
copy.AddLast(GetSomeObject());
collection = copy;
}
finally
{
if (acquired) Monitor.Exit(temp);
}
}
现在希望更容易看出如果我们使用collection
锁定表达式会出现什么问题。
- 线程 A 执行
object temp = collection
。
- 线程 B 执行
collection = copy
。
- 线程 C 执行
object temp = collection
。
- 线程 A 使用原始引用获取锁。
- 线程 C 使用新引用获取锁。
显然,这将是灾难性的!由于多次进入临界区,写入会丢失。
现在第二个问题有点棘手。您不一定必须使用我上面发布的代码来执行此操作。但是,那是因为我collection
只用过一次。现在考虑以下代码。
void Read()
{
object x = collection.Last;
// The collection may get swapped out right here.
object y = collection.Last;
if (x != y)
{
Console.WriteLine("It could happen!");
}
}
这里的问题是它collection
可能随时被换掉。这将是一个非常难以找到的错误。这就是为什么我在执行此模式时总是在读取端提取本地引用。这确保我们在每个读取操作上使用相同的集合。
同样,因为这些问题非常微妙,我不建议使用这种模式,除非你真的需要。