在没有锁的情况下读写盒装双值线程安全?

Reading and writing to boxed double values thread safe with no lock?

用 MS 的话来说,对 double 的读写(以及其他操作)不是原子的,因此不是线程安全的。我想看看我是否可以通过装箱双精度值并使对装箱值的引用可变来使读写双线程安全。下面是一个 class,它演示了我在工作线程和消费者线程中使用盒装双打:

public class Program {
    public volatile static object objCounter;
    public static AutoResetEvent Finish = new AutoResetEvent(false);
    static void Main(string[] args) {
        // Worker thread that writes to the boxed double values
        Task.Run(() => {
            double dCounter = 0d;
            while (!Finish.WaitOne(100, false)) {
                dCounter += 1.5d;

                // Box dCounter into objCounter -
                // Copy double value to new location on the heap
                // and point the objCounter to this location on the heap
                objCounter = (object)dCounter; 
            }
        });

        // Consumer thread that reads the boxed double values
        for (int index = 0; index < 50; index++) {
            Console.WriteLine(String.Format("Double counter value = {0}",objCounter));
            Thread.Sleep(100);
        }
        Finish.Set();
    }
}

我相信上面的代码是线程安全的,原因如下:

  1. 当工作线程将 double 写入 objCounter 时,它必须在堆上创建一个新位置,将 double 值复制到堆上的新位置,然后原子地复制对新堆位置的引用 objCounterobjCounter 的引用(指针)的原子副本保证直接从 MS 口中是原子的:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/variables#96-atomicity-of-variable-references –(请参阅 9.6 变量引用的原子性)重要的是要注意新位置每次我们将 double 装箱时都会在堆上创建,如果我们没有在堆上创建一个新位置,那么这将不是线程安全的。

  2. 由于 objCounter 是易变的,因此指向堆上包含当前双精度值的内存位置的引用的工作线程和使用者线程之间不会存在内存可见性问题。

我是否正确地假设我在上面指定的工作线程和消费者线程中使用盒装双打是线程安全的?

是的,您的代码是 thread-safe。由于 objCounter 是一个 volatile 字段,下面的两行:

objCounter = (object)dCounter;
Console.WriteLine(objCounter);

...等同于那些:

Volatile.Write(ref objCounter, (object)dCounter);
Console.WriteLine(Volatile.Read(ref objCounter));

两种Volatile方法都会产生内存障碍。 Volatile.Write ensures that the processor will not move the instructions assosiated with creating the objCounter instance, after this call. And the Volatile.Read 确保处理器不会在此调用之前移动与在控制台中显示 objCounter 相关的指令。因此显示 objCounter 的线程将始终看到一个完全初始化的实例。