如何在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(); } } }
关键说明
- 触发撕裂的核心逻辑:交替写入
null和long.MaxValue,每次写入都必须同时更新HasValue和Value两个字段。32位环境下,这两个字段的写入是独立的32位操作,中间被读取线程打断就会出现撕裂;64位环境下,虽然long的写入是原子的,但Nullable<T>整体的跨字段更新是非原子的,依然会触发撕裂。 - 读取端的检查逻辑:不仅检查表面的
currentValue是否合法,还通过不安全代码直接读取底层内存,确保能捕捉到跨字段的撕裂场景(比如HasValue是false但Value已经被更新,或者反过来)。 - 规避JIT优化:使用数组索引切换写入值,避免JIT将写入操作优化为仅更新单个字段,确保每次写入都操作两个字段。
规范依据
C#规范明确:结构体类型变量的读写操作,只有当结构体所有字段都支持原子访问时,整个操作才是原子的。Nullable<long>包含1字节的bool和8字节的long,整体读写是非原子的,因此必然存在撕裂风险。
内容的提问来源于stack exchange,提问作者mjwills
相关产品推荐
相关产品推荐

