使用泛型List线程安全吗

本文关键字:安全 线程 List 泛型 | 更新日期: 2023-09-27 18:00:55

我有一个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];
    }
}

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

使用泛型List线程安全吗

迭代队列并访问列表线程中相应项目的读取操作是否安全?

它是否被记录为线程安全的

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

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

如果你必须这样做,那么就使用一个专为共享内存访问而设计的集合。

如果不能做到这一点,则使用锁。锁如果不受控制的话很便宜。

如果因为锁一直被争用而出现性能问题,那么可以通过更改线程体系结构来解决这个问题,而不是尝试做一些危险和愚蠢的事情,比如低锁代码。除了少数专家之外,没有人能正确地编写低锁定代码。(我不是其中之一;我也不写低锁定代码。)

即使在对列表进行索引时调整了列表的大小,我也只是访问一个已经存在的项,所以无论是从旧数组还是从新数组读取它都无关紧要。

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

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

大编辑

ConcurrentQueue仅在Enqueue(T)和T Dequeue()操作方面是安全的。您正在对其执行foreach操作,但未在所需级别进行同步。在您的特定情况下,最大的问题是队列(本身就是一个集合)的枚举可能会引发众所周知的"集合已被修改"异常。为什么这是最大的问题?因为您是在之后向队列中添加东西的,所以您将相应的对象添加到列表中(也非常需要同步列表,但这+最大的问题只需一个"项目符号"即可解决)。在枚举集合时,不容易接受另一个线程正在修改它的事实(即使在微观级别上修改是安全的——ConcurrentQueue就是这样做的)。

因此,您绝对需要使用另一种同步方式来同步对队列(以及中央列表)的访问(我的意思是,您也可以忘记计算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%安全的。

大编辑结束

首先:你称之为线程安全的东西是什么?作者Eric Lippert

在您的特殊情况下,我想答案是

在全局上下文(实际列表)中可能不会出现不一致。

相反,实际的读者(他们很可能会与唯一的编写器"冲突")最终可能会出现自身的不一致(他们自己的Stacks意味着:所有方法的局部变量、参数以及堆中逻辑隔离的部分)。

这种"每个线程"不一致的可能性(第N个线程想要了解列表中的元素数量,并发现值为39404999,尽管实际上您只添加了3个值)足以声明,一般来说,体系结构是而不是线程安全的(尽管您实际上并没有更改全局可访问列表,只是通过以有缺陷的方式读取它)。

我建议您使用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 :)
}

否,因为您同时对同一对象进行读写操作。这没有被证明是安全的,所以你不能确定它是安全的。不要这么做。

事实上,到目前为止它是不安全的。NET 4.0毫无意义,顺便说一句。即使根据Reflector的说法它是安全的,它也可以随时更改您不能依赖当前版本来预测未来版本

不要试图用这样的伎俩逃脱惩罚。为什么不以一种明显安全的方式来做呢?

附带说明:两个定时器回调可以同时执行,因此您的代码会双重中断(多个编写器)不要试图通过线程来达到目的

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

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

这是另一种说法,即你的程序不会像使用Queue类时那样,随机弹出一条难以理解的异常消息。不过,要注意后果,这一保证中隐含的是,您很可能看到的是过时版本的队列。在循环开始执行后,您的循环将无法看到其他线程添加的任何元素。这种魔法并不存在,也不可能以一致的方式实现。这是否会让你的程序行为不端,这是你必须思考的问题,从这个问题中是猜不到的。你很少能完全忽略它

您对列表的使用<>然而是完全不安全的。