Should thread-safe class have a memory barrier at the end of its constructor? Should thread-safe class have a memory barrier at the end of its constructor? multithreading multithreading

Should thread-safe class have a memory barrier at the end of its constructor?


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


Should thread-safe class have a memory barrier at the end of itsconstructor?

I do not see a reason for this. The queue is local variable that is assigned from one thread and accessed from another. Such concurrent access should be synchronized and it is responsibility of the accessing code to do so. It has nothing to do with constructor or type of the variable, such access should always be explicitly synchronized or you are entering a dangerous area even for primitive types (even if the assignment is atomic, you may get caught is some cache trap). If the access to the variable is properly synchronized, it does not need any support in the constructor.


I'll attempt to answer this interesting and well-presented question, based on the comments by Servy and Douglas, and on information coming from other related questions. What follows is just my assumptions, and not solid information from a reputable source.

  1. Thread-safe classes have properties and methods that can be safely invoked by multiple threads concurrently, but their constructors are not thread-safe. This means that it is entirely possible for a thread to "see" an instance of a thread-safe class having an invalid state, provided that the instance is constructed concurrently by another thread.

  2. Adding the line Thread.MemoryBarrier(); at the end of the constructor is not enough to make the constructor thread-safe, because this statement only affects the thread that runs the constructor¹. The other threads that may access concurrently the under-construction instance are not affected. Memory-visibility is cooperative, and one thread cannot change what another thread "sees" by altering the other thread's execution flow (or invalidating the local cache of the CPU-core that the other thread is running on) in a non-cooperative manner.

  3. The correct and robust way to ensure that all threads are seeing the instance having a valid state, is to include proper memory barriers in all threads. This can be achieved by either declaring the instance as volatile, in case it is a field of a class, or otherwise using the methods of the static Volatile class:

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

In this particular example it would be simpler and more efficient to instantiate the queue variable before invoking the Parallel.For method. Doing so would render unnecessary the explicit Volatile invocations. The Parallel.For method is using Tasks internally, and TPL includes the appropriate memory barriers at the beginning/end of each task. Memory barriers are generated implicitly and automatically by the .NET infrastructure, by any built-in mechanism that starts a thread or causes a delegate to execute on another thread. (citation)

I'll repeat that I'm not 100% confident about the correctness of the information presented above.

¹ Quoting from the documentation of the Thread.MemoryBarrier method: Synchronizes memory access as follows: The processor executing the current thread cannot reorder instructions in such a way that memory accesses prior to the call to MemoryBarrier() execute after memory accesses that follow the call to MemoryBarrier().