异常可能是由于在迭代过程中通过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 使用 new 引用获取锁。
显然这将是灾难性的!由于多次进入临界区,写入会丢失。
现在第二个问题有点棘手。您不一定必须使用我在上面发布的代码来执行此操作。但是,那是因为我只使用了一次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 随时可能被换掉。这将是一个难以置信难以找到的错误。这就是为什么我在执行此模式时总是在读取端提取本地引用。这确保我们在每个读取操作上都使用 same 集合。
再说一次,因为此类问题非常微妙,我不建议您使用这种模式,除非您真的需要。