You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Nullable<long>类型上引发多线程撕裂问题?

问题

在Windows 11的32位.NET Framework 4.7.2环境下,测试代码里long类型变量会出现预期的多线程撕裂现象,但Nullable<long>却没有触发。按照C#规范,Nullable<long>理论上也存在撕裂风险,需要实现两个目标:

  • 仅通过单个写入器引发Nullable<long>的撕裂
  • 在64位环境下复现该问题

原测试代码:

using System;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    public class Program
    {
        public static void Main(string[] args)
        {
            long bob = 0;
            //long? bob = 0;

            long small = 0;
            long large = long.MaxValue;


            Task.Run(() =>
            {
                do
                {
                    bob = small;
                    bob = large;
                } while (true);
            });

            for (long i = 0; i < long.MaxValue - 2; i++)
            {
                var value = bob;

                if (value != small && value != large)
                {
                    Console.WriteLine("very bad");
                }
            }

            Console.WriteLine("finished");
            Console.ReadLine();
        }
    }
}
原因分析与解决方案

为什么原代码没触发Nullable<long>撕裂?

Nullable<T>本质是包含两个字段的结构体:bool HasValue和T Value。原代码里写入的0和long.MaxValue对应的HasValue都是true,JIT可能会把写入操作优化成只更新Value字段(因为HasValue没有变化),自然不会出现跨字段的撕裂。

要触发撕裂,必须让写入操作同时修改HasValue和Value——比如在null和非null值之间交替写入,或者让JIT无法优化字段更新逻辑。

修改后的测试代码(32/64位通用)

using System;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    public class Program
    {
        static long? bob = null;
        // 交替写入null和long.MaxValue,强制修改两个字段
        static readonly long?[] WriteValues = { null, long.MaxValue };

        public static void Main(string[] args)
        {
            // 单个写入器线程
            Task.Run(() =>
            {
                int toggleIndex = 0;
                do
                {
                    bob = WriteValues[toggleIndex];
                    toggleIndex ^= 1; // 切换索引,在null和MaxValue间循环写入
                } while (true);
            });

            // 读取线程持续检查撕裂
            while (true)
            {
                var currentValue = bob;
                // 正常情况要么是null,要么是long.MaxValue
                if (currentValue.HasValue && currentValue.Value != long.MaxValue)
                {
                    Console.WriteLine($"撕裂发生!读取结果:HasValue={currentValue.HasValue}, Value={currentValue.Value}");
                    break;
                }
                // 额外检查:HasValue为false但底层Value有值的情况
                else if (!currentValue.HasValue)
                {
                    unsafe
                    {
                        long rawValue = *(long*)&bob;
                        if (rawValue != 0)
                        {
                            Console.WriteLine($"撕裂发生!HasValue=false,但底层Value={rawValue}");
                            break;
                        }
                    }
                }
            }

            Console.ReadLine();
        }
    }
}

关键说明

  1. 触发撕裂的核心逻辑:交替写入null和long.MaxValue,每次写入都必须同时更新HasValue和Value两个字段。32位环境下,这两个字段的写入是独立的32位操作,中间被读取线程打断就会出现撕裂;64位环境下,虽然long的写入是原子的,但Nullable<T>整体的跨字段更新是非原子的,依然会触发撕裂。
  2. 读取端的检查逻辑:不仅检查表面的currentValue是否合法,还通过不安全代码直接读取底层内存,确保能捕捉到跨字段的撕裂场景(比如HasValue是false但Value已经被更新,或者反过来)。
  3. 规避JIT优化:使用数组索引切换写入值,避免JIT将写入操作优化为仅更新单个字段,确保每次写入都操作两个字段。

规范依据

C#规范明确:结构体类型变量的读写操作,只有当结构体所有字段都支持原子访问时,整个操作才是原子的。Nullable<long>包含1字节的bool和8字节的long,整体读写是非原子的,因此必然存在撕裂风险。

内容的提问来源于stack exchange,提问作者mjwills

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 05:04:58