8

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

4 回答 4

9

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

它是否记录为线程安全?

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

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

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

如果您不能这样做,请使用 lock。如果没有竞争,锁很便宜。

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

即使列表在被索引时调整大小,我也只是访问一个已经存在的项目,所以无论是从旧数组还是新数组中读取它都没有关系。

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

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

于 2013-02-24T15:46:36.197 回答
6

大编辑

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

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

大编辑结束

首先:你称之为 Thread-Safe 的东西是什么?埃里克·利珀特

在您的特定情况下,我想答案是否定的。

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

取而代之的是,实际的读者(他们很可能与唯一的作者“冲突”)最终会在自身内部出现不一致(他们自己的堆栈含义:所有方法的局部变量、参数以及它们在堆中的逻辑隔离部分) ))。

这种“每线程”不一致的可能性(第 N 个线程想要了解 List 中元素的数量并发现该值为 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 :)
}
于 2013-02-24T14:19:13.490 回答
1

不,因为您正在同时读取和写入同一个对象。这没有被证明是安全的,所以你不能确定它是安全的。不要这样做。

顺便说一句,从 .NET 4.0 开始它实际上是不安全的这一事实毫无意义。即使根据反射器它是安全的,它也可能随时改变。您不能依赖当前版本来预测未来版本。

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

附带说明:两个计时器回调可以同时执行,因此您的代码被双重破坏(多个编写者)。不要试图用线程来耍花招。

于 2013-02-24T15:15:27.577 回答
1

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

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

这是另一种说法,即您的程序不会像使用 Queue 类时会得到的那种难以理解的异常消息随机爆炸。但请注意后果,此保证中隐含的是您很可能正在查看队列的陈旧版本。在您的循环开始执行后,您的循环将无法看到其他线程添加的任何元素。这种魔法不存在,也不可能以一致的方式实施。这是否会使您的程序行为不端是您必须考虑的事情,并且无法从问题中猜到。您可以完全忽略它是非常罕见的。

然而,您对 List<> 的使用是完全不安全的。

于 2013-02-24T16:03:02.007 回答