K.M*_*.M. 7 .net c# 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];
}
}
Run Code Online (Sandbox Code Playgroud)
我的理解:即使列表在索引时被调整大小,我只访问已经存在的项目,因此无论是从旧数组还是新数组中读取它都无关紧要.因此,这应该是线程安全的.
是一个读取操作,它迭代队列并访问列表线程中的相应项目安全吗?
是否记录为线程安全?
如果不是,那么将它视为线程安全是愚蠢的,即使它是偶然的.螺纹安全应该是设计的.
首先在线程之间共享内存是一个坏主意; 如果你不这样做,那么你不必询问该操作是否是线程安全的.
如果必须这样做,请使用专为共享内存访问而设计的集合.
如果你不能这样做那么使用锁.如果不加协议,锁便宜.
如果您遇到性能问题,因为您的锁始终存在争议,那么通过更改线程体系结构而不是尝试执行低锁代码等危险和愚蠢的操作来解决该问题.除少数专家外,没有人能正确编写低锁代码.(我不是其中之一;我也不写低锁代码.)
即使列表在被索引时被调整大小,我只访问已经存在的项目,因此无论是从旧数组还是新数组中读取它都无关紧要.
这是错误的思考方式.正确的思考方式是:
如果调整列表大小,则列表的内部数据结构将发生变异.内部数据结构有可能在突变中途变异为不一致的形式,在变异完成时将变得一致.因此,我的读者可以从另一个线程看到这种不一致的状态,这使得整个程序的行为变得不可预测.它可能会崩溃,它可能会进入一个无限循环,它可能会破坏其他数据结构,我不知道,因为我运行的代码在一个状态不一致的世界中呈现一致状态.
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();
}
}
Run Code Online (Sandbox Code Playgroud)
并在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;
}
Run Code Online (Sandbox Code Playgroud)
并且,基于我对您的应用程序访问列表和各种队列的情况的完整理解,它应该是100%安全的.
首先:你称之为线程安全的东西是什么?作者:Eric Lippert
在你的特殊情况下,我猜答案是否定的.
在全球范围内(实际清单)可能不会出现不一致的情况.
相反,有可能实际的读者(他们可能很好地与独特的作者"碰撞")最终会出现不一致(他们自己的Stacks含义:所有方法的局部变量,参数以及它们在逻辑上孤立的堆部分) )).
这种"每线程"不一致的可能性(第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 :)
}
Run Code Online (Sandbox Code Playgroud)