我知道之前已经问过这个问题(我将继续研究),但我需要知道如何以线程安全的方式创建特定的链表功能。我当前的问题是我有一个循环遍历链表中所有元素的线程,另一个可能会在此列表的末尾添加更多元素。有时会发生一个线程尝试将另一个元素添加到列表中,而第一个元素正在忙于迭代它(这会导致异常)。
我在考虑添加一个变量(布尔标志)来表示列表当前正在忙于迭代,但是我如何检查它并等待第二个线程(如果它等待,则可以,因为第一个线程运行得很快)。我能想到的唯一方法是通过使用while循环不断检查这个忙碌的标志。我意识到这是一个非常愚蠢的想法,因为它会导致CPU在没有任何用处的情况下努力工作。现在我在这里要求更好的见解。我已经阅读了关于锁等的内容,但在我的情况下似乎并不相关,但也许我错了?
与此同时,如果我找到解决方案的话,我会继续搜索互联网并发回。
编辑: 让我知道是否应该发布一些代码来清理,但我会尝试更清楚地解释它。
所以我有一个带有链表的类,其中包含需要处理的元素。我有一个线程通过函数调用遍历此列表(让我们称之为“processElements”)。我有第二个线程,以非确定的方式添加元素进行处理。但是,有时它会在processElements运行时尝试调用此addElement函数。这意味着当一个元素被第一个线程迭代时,它被添加到链表中。这是不可能的,并导致异常。希望这可以解决它。
我需要添加新元素的线程,直到processElements方法执行完毕为止。
答案 0 :(得分:23)
异常可能是通过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
作为锁定表达式,会更容易看出会出现什么问题。
object temp = collection
。collection = copy
。object temp = collection
。现在第二个问题有点棘手。您不一定 使用上面发布的代码执行此操作。但是,那是因为我只使用了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
可能随时被换出。这将是一个令人难以置信的难以找到的bug。这就是我在执行此模式时总是在读取端提取本地引用的原因。这确保我们在每次读取操作中使用相同的集合。
同样,由于这些问题非常微妙,我建议不要使用此模式,除非确实需要。
答案 1 :(得分:5)
以下是如何使用锁定来同步对列表的访问的快速示例:
private readonly IList<string> elements = new List<string>();
public void ProcessElements()
{
lock (this.elements)
{
foreach (string element in this.elements)
ProcessElement(element);
}
}
public void AddElement(string newElement)
{
lock (this.elements)
{
this.elements.Add(element);
}
}
lock(o)
语句表示执行线程应该在对象o
上获取互斥锁,执行语句块,最后释放o
上的锁。如果另一个线程试图同时获取o
上的锁(对于相同的代码块或任何其他代码块),则它将阻塞(等待)直到锁被释放。
因此,关键点在于您对要同步的所有lock
语句使用相同的对象。您使用的实际对象可以是任意的,只要它是一致的。在上面的例子中,我们声明我们的集合是只读的,所以我们可以安全地使用它作为我们的锁。但是,如果不是这种情况,则应锁定另一个对象:
private IList<string> elements = new List<string>();
private readonly object syncLock = new object();
public void ProcessElements()
{
lock (this.syncLock)
{
foreach (string element in this.elements)
ProcessElement(element);
}
}
public void AddElement(string newElement)
{
lock (this.syncLock)
{
this.elements.Add(element);
}
}