是否使用通用List线程是安全的

时间:2013-02-24 14:08:19

标签: c# .net thread-safety

我有System.Collections.Generic.List<T>我只在计时器回调中添加项目。只有在操作完成后才会重新启动计时器。

我有一个System.Collections.Concurrent.ConcurrentQueue<T>,它存储了上面列表中添加项目的索引。此存储操作也始终在上述相同的计时器回调中执行。

是一个读取操作,它迭代队列并访问列表线程安全中的相应项吗?

示例代码:

private List<Object> items;
private ConcurrentQueue<int> queue;
private Timer timer;
private void callback(object state)
{
    int index = items.Count;
    items.Add(new object());
    if (true)//some condition here
        queue.Enqueue(index);
    timer.Change(TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(-1));
}

//This can be called from any thread
public IEnumerable<object> AccessItems()
{
    foreach (var index in queue)
    {
        yield return items[index];
    }
}

我的理解: 即使列表在被索引时被调整大小,我只访问已经存在的项目,因此无论是从旧数组还是新数组中读取它都无关紧要。因此,这应该是线程安全的。

4 个答案:

答案 0 :(得分:9)

  

是一个读取操作,它迭代队列并访问列表线程安全中的相应项吗?

记录是否是线程安全的?

如果不是,那么将它视为线程安全是愚蠢的,即使它偶然在这个实现中。线程安全应该设计

首先在线程之间共享内存是一个坏主意;如果你不这样做,那么你不必询问该操作是否是线程安全的。

如果必须这样做,请使用专为共享内存访问而设计的集合。

如果你不能这样做,那么使用锁。如果不加协议,锁便宜。

如果您遇到性能问题,因为您的锁始终存在争议,那么通过更改线程架构来解决问题,而不是尝试执行低锁代码等危险和愚蠢的操作。除少数专家外,没有人能正确编写低锁代码。 (我不是其中之一;我也不写低锁代码。)

  

即使列表在索引时被调整大小,我也只访问已经存在的项目,因此无论是从旧数组还是新数组中读取它都无关紧要。

这是错误的思考方式。正确的思考方式是:

  

如果调整列表大小,则列表的内部数据结构将发生变异。内部数据结构有可能在突变中途变异为不一致的形式,在变异完成时将变得一致。因此,我的读者可以从另一个线程看到这种不一致的状态,这使得整个程序的行为无法预测。它可能会崩溃,它可能会进入一个无限循环,它可能会破坏其他数据结构,我不知道,因为我运行的代码在状态不一致的世界中假设一致状态

答案 1 :(得分:5)

大编辑

ConcurrentQueue对于Enqueue(T)和T Dequeue()操作是唯一安全的。 您正在对其执行 foreach ,并且无法在所需级别进行同步。 在您的特定情况下,最大的问题是,Queue的枚举(它本身就是一个Collection)可能会抛出众所周知的“Collection已被修改”异常。为什么这是最大的问题?因为您在之后向队列添加了东西,所以你已经将相应的对象添加到列表中了(同样需要同步List,但是+最大的问题只需一个就能解决)子弹”)。在枚举集合时,不容易吞下另一个线程正在修改它的事实(即使在微观层面上修改是安全的 - ConcurrentQueue就是这样做的。)

因此,您绝对需要使用其他同步方法同步对队列(以及中心列表)的访问权限(并且我的意思是您也可以忘记使用简单的队列甚至是一个简单的队列列表,因为你永远不会出现事情)。

所以只需做一些事情:

public void Writer(object toWrite) {
  this.rwLock.EnterWriteLock();
  try {
    int tailIndex = this.list.Count;
    this.list.Add(toWrite);

    if (..condition1..)
      this.queue1.Enqueue(tailIndex);
    if (..condition2..)
      this.queue2.Enqueue(tailIndex);
    if (..condition3..)
      this.queue3.Enqueue(tailIndex);
    ..etc..
  } finally {
    this.rwLock.ExitWriteLock();
  }
}

并在AccessItems中:

public IEnumerable<object> AccessItems(int queueIndex) {
  Queue<object> whichQueue = null;
  switch (queueIndex) {
    case 1: whichQueue = this.queue1; break;
    case 2: whichQueue = this.queue2; break;
    case 3: whichQueue = this.queue3; break;
    ..etc..
    default: throw new NotSupportedException("Invalid queue disambiguating params");    
  }

  List<object> results = new List<object>();
  this.rwLock.EnterReadLock();
  try {
    foreach (var index in whichQueue)
      results.Add(this.list[index]);
  } finally {
    this.rwLock.ExitReadLock();
  }

  return results;
}

并且,基于我对您的应用访问列表和各种队列的案例的完整理解,它应该是100%安全的。

大编辑结束

首先:What is this thing you call Thread-Safe ? by Eric Lippert

在您的特定情况下,我猜答案是

在全球范围内(实际列表)可能不会出现不一致的情况。

相反,有可能实际的读者(他们可能与独特的作家很好地“碰撞”)最终会出现不一致(他们自己的Stacks含义:所有方法的局部变量,参数以及它们在逻辑上相互独立的部分堆))。

这种“每线程”不一致的可能性(第N个线程想要了解List中的元素数量并发现该值为39404999,尽管实际上你只添加了3个值)足以声明,一般说这个体系结构是线程安全的(尽管你实际上并没有改变全局可访问的List,只需要以有缺陷的方式阅读它)。

我建议您使用ReaderWriterLockSlim课程。 我想你会发现它符合你的需求:

private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

private List<Object> items;
private ConcurrentQueue<int> queue;
private Timer timer;
private void callback(object state)
{
  int index = items.Count;

  this.rwLock.EnterWriteLock();
  try {
    // in this place, right here, there can be only ONE writer
    // and while the writer is between EnterWriteLock and ExitWriteLock
    // there can exist no readers in the following method (between EnterReadLock
    // and ExitReadLock)

    // we add the item to the List
    // AND do the enqueue "atomically" (as loose a term as thread-safe)
    items.Add(new object());

    if (true)//some condition here
      queue.Enqueue(index);
  } finally {
    this.rwLock.ExitWriteLock();
  }

  timer.Change(TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(-1));
}

//This can be called from any thread
public IEnumerable<object> AccessItems()
{
  List<object> results = new List<object>();

  this.rwLock.EnterReadLock();
  try {
    // in this place there can exist a thousand readers 
    // (doing these actions right here, between EnterReadLock and ExitReadLock)
    // all at the same time, but NO writers
    foreach (var index in queue)
    {
      this.results.Add ( this.items[index] );
    }        
  } finally {
    this.rwLock.ExitReadLock();
  }


  return results; // or foreach yield return you like that more :)
}

答案 2 :(得分:1)

不,因为您正在同时读取和写入同一个对象。这没有记录是安全的,因此您无法确定它是否安全。不要这样做。

事实上它实际上不安全,因为.NET 4.0没有任何意义,顺便说一句。即使根据Reflector它是安全的,它也可以随时改变。 您不能依赖当前版本来预测未来的版本。

不要试图逃避这样的伎俩。为什么不以明显安全的方式来做呢?

作为旁注:两个计时器回调可以同时执行,因此您的代码会被双重打破(多个编写器)。 不要试图用线程来扯掉技巧。

答案 3 :(得分:1)

这是线程安全的。 foreach语句使用ConcurrentQueue.GetEnumerator()方法。承诺:

  

枚举表示队列内容的即时快照。调用GetEnumerator后,它不会反映对集合的任何更新。枚举器可以安全地与队列的读取和写入同时使用。

这是另一种说法,你的程序不会随意乱写一个不可思议的异常消息,就像你使用Queue类时会得到的那样。但要注意后果,隐藏在这个保证中的是你可能正在查看队列的陈旧版本。在循环开始执行后,您的循环将无法看到由另一个线程添加的任何元素。这种魔法不存在,不可能以一致的方式实施。这是否会使你的程序行为异常是你必须要考虑的事情,而不能从这个问题中猜到。您完全可以忽略它是非常罕见的。

您对List&lt;&gt;的使用然而,完全不安全。