线程安全类是否应该在其构造函数的末尾有一个内存屏障? [英] Should thread-safe class have a memory barrier at the end of its constructor?

查看:14
本文介绍了线程安全类是否应该在其构造函数的末尾有一个内存屏障?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

When implementing a class intended to be thread-safe, should I include a memory barrier at the end of its constructor, in order to ensure that any internal structures have completed being initialized before they can be accessed? Or is it the responsibility of the consumer to insert the memory barrier before making the instance available to other threads?

简化问题:

下面的代码中是否存在可能由于初始化和线程安全类访问之间缺少内存屏障而导致错误行为的竞争风险?还是线程安全类本身应该防止这种情况发生?

Is there a race hazard in the code below that could give erroneous behaviour due to the lack of a memory barrier between the initialization and the access of the thread-safe class? Or should the thread-safe class itself protect against this?

ConcurrentQueue<int> queue = null;

Parallel.Invoke(
    () => queue = new ConcurrentQueue<int>(),
    () => queue?.Enqueue(5));

请注意,程序不进行任何排队是可以接受的,如果第二个委托在第一个委托之前执行就会发生这种情况.(空条件运算符 ?. 在此处防止出现 NullReferenceException.)但是,程序抛出 IndexOutOfRangeException 是不可接受的,NullReferenceException,多次入队 5,陷入无限循环,或者做任何其他由内部结构的竞争风险引起的奇怪事情.

Note that it is acceptable for the program to enqueue nothing, as would happen if the second delegate executes before the first. (The null-conditional operator ?. protects against a NullReferenceException here.) However, it should not be acceptable for the program to throw an IndexOutOfRangeException, NullReferenceException, enqueue 5 multiple times, get stuck in an infinite loop, or do any of the other weird things caused by race hazards on internal structures.

详细问题:

具体地说,想象一下我正在为一个队列实现一个简单的线程安全包装器.(我知道 .NET 已经提供了 ConcurrentQueue;这只是一个例子.)我可以写:

Concretely, imagine that I were implementing a simple thread-safe wrapper for a queue. (I'm aware that .NET already provides ConcurrentQueue<T>; this is just an example.) I could write:

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 初始化之前访问实例.作为一个人为的例子:

This implementation is thread-safe, once initialized. However, if the initialization itself is raced by another consumer thread, then race hazards could arise, whereby the latter thread would access the instance before the internal Queue<T> has been initialized. As a contrived example:

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 尚未初始化而得到 NullReferenceException(或其他一些奇怪的结果)EnqueueTryDequeue 被调用.

It is acceptable for the code above to miss some numbers; however, without the memory barrier, it could also be getting a NullReferenceException (or some other weird result) due to the internal Queue<T> not having been initialized by the time that Enqueue or TryDequeue are called.

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

Is it the responsibility of the thread-safe class to include a memory barrier at the end of its constructor, or is it the consumer who should include a memory barrier between the class's instantiation and its visibility to other threads? What is the convention in the .NET Framework for classes marked as thread-safe?

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

Edit: This is an advanced threading topic, so I understand the confusion in some of the comments. An instance can appear as half-baked if accessed from other threads without proper synchronization. This topic is discussed extensively within the context of double-checked locking, which is broken under the ECMA CLI specification without the use of memory barriers (such as through volatile). Per Jon Skeet:

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

The Java memory model doesn't ensure that the constructor completes before the reference to the new object is assigned to instance. The Java memory model underwent a reworking for version 1.5, but double-check locking is still broken after this without a volatile variable (as in C#).

没有任何内存屏障,它在 ECMA CLI 规范中也被打破了.在 .NET 2.0 内存模型(比 ECMA 规范更强)下它可能是安全的,但我宁愿不依赖那些更强的语义,特别是如果对安全性有任何疑问.

Without any memory barriers, it's broken in the ECMA CLI specification too. It's possible that under the .NET 2.0 memory model (which is stronger than the ECMA spec) it's safe, but I'd rather not rely on those stronger semantics, especially if there's any doubt as to the safety.

推荐答案

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

Lazy<T> is a very good choice for Thread-Safe Initialization. I think it should be left to the consumer to provide that:

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);
    }
});

这篇关于线程安全类是否应该在其构造函数的末尾有一个内存屏障?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆