C# 线程安全类应该在其构造函数的末尾有一个内存屏障吗?

C# 线程安全类应该在其构造函数的末尾有一个内存屏障吗?,c#,.net,multithreading,parallel-processing,memory-barriers,C#,.net,Multithreading,Parallel Processing,Memory Barriers,当实现一个线程安全的类时,我是否应该在其构造函数的末尾包含一个内存屏障,以确保任何内部结构在被访问之前都已完成初始化?还是使用者有责任在将实例提供给其他线程之前插入内存屏障 简化问题: 由于初始化和线程安全类的访问之间缺乏内存屏障,下面的代码中是否存在可能导致错误行为的争用危险?或者线程安全类本身应该对此进行保护吗 ConcurrentQueue<int> queue = null; Parallel.Invoke( () => queue = new Concurr

当实现一个线程安全的类时,我是否应该在其构造函数的末尾包含一个内存屏障,以确保任何内部结构在被访问之前都已完成初始化?还是使用者有责任在将实例提供给其他线程之前插入内存屏障

简化问题

由于初始化和线程安全类的访问之间缺乏内存屏障,下面的代码中是否存在可能导致错误行为的争用危险?或者线程安全类本身应该对此进行保护吗

ConcurrentQueue<int> queue = null;

Parallel.Invoke(
    () => queue = new ConcurrentQueue<int>(),
    () => queue?.Enqueue(5));
一旦初始化,此实现是线程安全的。但是,如果初始化本身被另一个使用者线程争用,则可能会出现争用危险,即后者线程将在内部
队列
初始化之前访问实例。作为一个人为的例子:

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);
    }
});
ThreadSafeQueue=null;
对于(0,10000,i=>
{
如果(i==0)
队列=新的线程安全队列();
else如果(i%2==0)
排队?排队(i);
其他的
{
int项=-1;
if(队列?.TryDequeue(out项)==true)
控制台写入线(项目);
}
});
上述代码遗漏一些数字是可以接受的;但是,如果没有内存屏障,它也可能会得到
NullReferenceException
(或其他一些奇怪的结果),因为内部
队列
在调用
排队
TryDequeue
时尚未初始化

线程安全类的责任是在其构造函数的末尾包含内存屏障,还是应该在类的实例化和对其他线程的可见性之间包含内存屏障的使用者?在.NET Framework中,标记为线程安全的类的约定是什么

编辑:这是一个高级线程主题,因此我理解一些评论中的困惑。如果在没有适当同步的情况下从其他线程访问实例,则实例可能会显示为半生不熟。本主题在双重检查锁定的上下文中进行了广泛讨论,双重检查锁定在ECMA CLI规范下被打破,而不使用内存屏障(例如通过
volatile
)。Per:

Java内存模型不能确保构造函数在将对新对象的引用分配给实例之前完成。Java内存模型在版本1.5中进行了重新设计,但是在没有易失性变量的情况下(如C#),双重检查锁定仍然被破坏

没有任何内存障碍,它在ECMA CLI规范中也被打破。在.NET2.0内存模型(比ECMA规范更强大)下,它可能是安全的,但我不想依赖那些更强大的语义,尤其是如果对安全性有任何疑问的话

Lazy
是线程安全初始化的一个非常好的选择。我认为应该让消费者提供:

var queue = new Lazy<ThreadSafeQueue<int>>(() => new ThreadSafeQueue<int>());

Parallel.For(0, 10000, i =>
{

    else if (i % 2 == 0)
        queue.Value.Enqueue(i);
    else
    {
        int item = -1;
        if (queue.Value.TryDequeue(out item) == true)
            Console.WriteLine(item);
    }
});
var queue=new Lazy(()=>new ThreadSafeQueue());
对于(0,10000,i=>
{
else如果(i%2==0)
queue.Value.Enqueue(i);
其他的
{
int项=-1;
if(queue.Value.TryDequeue(out项)==true)
控制台写入线(项目);
}
});
不,构造函数中不需要内存屏障。你的假设,即使展示了一些创造性思维,也是错误的。没有线程可以获取
队列的半备份实例。新引用仅在初始化完成时对其他线程“可见”。假设线程_1是初始化
队列
的第一个线程-它经过ctor代码,但是
队列
在主堆栈中的引用仍然为空!只有当线程_1存在构造函数代码时,它才会分配引用


请参阅下面的评论和详细阐述的问题。

我将根据Servy和Douglas的评论,以及其他相关问题的信息,尝试回答这个有趣且呈现良好的问题。以下只是我的假设,并非来自可靠来源的可靠信息

  • 线程安全类具有可由多个线程同时安全调用的属性和方法,但它们的构造函数不是线程安全的。这意味着,如果实例由另一个线程并发构造,线程完全有可能“看到”具有无效状态的线程安全类的实例


  • 添加行
    Thread.MemoryBarrier()不足以使构造函数线程安全,因为此语句只影响运行构造函数的线程。可能同时访问在建实例的其他线程不受影响。内存可见性是协作的,一个线程不能通过以非协作方式更改另一个线程的执行流(或使另一个线程运行的CPU内核的本地缓存无效)来更改另一个线程“看到”的内容

  • 确保所有线程都看到实例具有有效状态的正确而健壮的方法是在所有线程中包含适当的内存屏障。这可以通过将实例声明为(如果它是类的字段)或使用静态类的方法来实现:

  • ThreadSafeQueue=null;
    对于(0,10000,i=>
    {
    如果(i==0)
    Volatile.Write(ref-queue,new-ThreadSafeQueue());
    else如果(i%2==0)
    Volatile.Read(ref队列)?.Enqueue(i);
    其他的
    {
    int项=-1;
    if(Volatile.Read(ref队列)?.TryDequeue(out项)==true)
    控制台写入线(项目);
    }
    });
    
    在这个特殊的例子中
    var queue = new Lazy<ThreadSafeQueue<int>>(() => new ThreadSafeQueue<int>());
    
    Parallel.For(0, 10000, i =>
    {
    
        else if (i % 2 == 0)
            queue.Value.Enqueue(i);
        else
        {
            int item = -1;
            if (queue.Value.TryDequeue(out item) == true)
                Console.WriteLine(item);
        }
    });
    
    ThreadSafeQueue<int> queue = null;
    
    Parallel.For(0, 10000, i =>
    {
        if (i == 0)
            Volatile.Write(ref queue, new ThreadSafeQueue<int>());
        else if (i % 2 == 0)
            Volatile.Read(ref queue)?.Enqueue(i);
        else
        {
            int item = -1;
            if (Volatile.Read(ref queue)?.TryDequeue(out item) == true)
                Console.WriteLine(item);
        }
    });