使链接列表线程安全

时间:2012-05-11 18:36:21

标签: c# multithreading

我知道之前已经问过这个问题(我将继续研究),但我需要知道如何以线程安全的方式创建特定的链表功能。我当前的问题是我有一个循环遍历链表中所有元素的线程,另一个可能会在此列表的末尾添加更多元素。有时会发生一个线程尝试将另一个元素添加到列表中,而第一个元素正在忙于迭代它(这会导致异常)。

我在考虑添加一个变量(布尔标志)来表示列表当前正在忙于迭代,但是我如何检查它并等待第二个线程(如果它等待,则可以,因为第一个线程运行得很快)。我能想到的唯一方法是通过使用while循环不断检查这个忙碌的标志。我意识到这是一个非常愚蠢的想法,因为它会导致CPU在没有任何用处的情况下努力工作。现在我在这里要求更好的见解。我已经阅读了关于锁等的内容,但在我的情况下似乎并不相关,但也许我错了?

与此同时,如果我找到解决方案的话,我会继续搜索互联网并发回。

编辑: 让我知道是否应该发布一些代码来清理,但我会尝试更清楚地解释它。

所以我有一个带有链表的类,其中包含需要处理的元素。我有一个线程通过函数调用遍历此列表(让我们称之为“processElements”)。我有第二个线程,以非确定的方式添加元素进行处理。但是,有时它会在processElements运行时尝试调用此addElement函数。这意味着当一个元素被第一个线程迭代时,它被添加到链表中。这是不可能的,并导致异常。希望这可以解决它。

我需要添加新元素的线程,直到processElements方法执行完毕为止。

  • 任何绊倒这个问题的人。接受的答案将为您提供快速,简单的解决方案,但请查看下面的Brian Gideon的答案,以获得更全面的答案,这肯定会为您提供更多见解!

2 个答案:

答案 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作为锁定表达式,会更容易看出会出现什么问题。

  • 主题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可能随时被换出。这将是一个令人难以置信的难以找到的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);
    }
}