ConcurrentQueue<int> queue = null;
Parallel.Invoke(
() => queue = new ConcurrentQueue<int>(),
() => queue?.Enqueue(5));
请注意,程序可以不将任何内容加入队列,这种情况可能会在第二个委托在第一个委托之前执行时发生。(空值条件运算符
?.
可防止此处出现NullReferenceException
。)但是,程序不应该抛出IndexOutOfRangeException
、NullReferenceException
、多次将5
加入队列、陷入无限循环或做任何其他由内部结构竞争危害引起的奇怪事情。详细问题:
具体而言,想象一下我正在为队列实现一个简单的线程安全包装器。(我知道.NET已经提供了
ConcurrentQueue<T>
;这只是一个例子。)我可以编写:public class ThreadSafeQueue<T>
{
private readonly Queue<T> _queue;
public ThreadSafeQueue()
{
_queue = new Queue<T>();
// Thread.MemoryBarrier(); // Is this line required?
}
public void Enqueue(T item)
{
lock (_queue)
{
_queue.Enqueue(item);
}
}
public bool TryDequeue(out T item)
{
lock (_queue)
{
if (_queue.Count == 0)
{
item = default(T);
return false;
}
item = _queue.Dequeue();
return true;
}
}
}
一旦初始化,此实现是线程安全的。但是,如果初始化本身被另一个消费者线程竞争,则可能会出现竞争危害,后者线程将在内部 Queue<T>
初始化之前访问该实例。以下是一个人为的示例:
ThreadSafeQueue<int> queue = null;
Parallel.For(0, 10000, i =>
{
if (i == 0)
queue = new ThreadSafeQueue<int>();
else if (i % 2 == 0)
queue?.Enqueue(i);
else
{
int item = -1;
if (queue?.TryDequeue(out item) == true)
Console.WriteLine(item);
}
});
上面的代码可能会遗漏一些数字,但是如果没有内存屏障,由于内部的
Queue<T>
在调用Enqueue
或TryDequeue
时尚未初始化,也可能会出现NullReferenceException
(或其他奇怪的结果)。线程安全类是否有责任在构造函数末尾包含内存屏障,还是消费者应该在类的实例化和对其他线程的可见性之间包含内存屏障?在.NET Framework中标记为线程安全的类的惯例是什么?
编辑:这是一个高级的线程主题,所以我理解一些评论中的困惑。如果没有适当的同步,实例从其他线程访问时可能会出现半成品。这个主题在双重检查锁定的上下文中广泛讨论,在ECMA CLI规范下,如果没有使用内存屏障(例如通过
volatile
),则双重检查锁定是无效的。根据Jon Skeet的说法:Java内存模型不能确保构造函数在将新对象的引用分配给实例之前完成。 Java内存模型在1.5版本进行了重新设计,但是在没有volatile变量的情况下,双重检查锁定仍然存在问题(就像C#中一样)。如果没有任何内存屏障,ECMA CLI规范也无法解决该问题。可能在.NET 2.0内存模型(比ECMA规范更强)下是安全的,但是如果有任何安全方面的疑虑,最好不要依赖这些更强的语义。
ConcurrentQueue<T>
的源代码在其构造函数中没有任何保护措施。这是你自己的判断。http://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentQueue.cs,18bcbcbdddbcfdcb - Bradley Uffner