C#结构体访问出现锁竞争:来源排查及保留结构体的解决方法
结构体访问出现锁竞争的原因与解决方法
我在C#中定义了如下结构体:
public struct UnitData { public int ID { get; init; } public Vector2 XY { get; set; } }
性能分析时发现,访问XY字段(以及该结构体的其他数据)存在大量锁竞争耗时。将实现从struct改为class后,这个开销就消失了。我的代码里没有加任何锁,程序虽是多线程架构,但操作该数据的代码只在单个后台线程执行。查看中间语言(IL)代码,使用该结构体的代码和属性本身都没有Monitor.Enter或Monitor.Exit的调用。
问题:如果锁不是来自我的代码,它的来源是什么?如何在保留结构体的前提下消除该开销?
XY的示例用法:
public IEnumerable<int> MoveNext(Vector2 location, float radius) { foreach (UnitData item in m_units.Items) { if (Vector2.Distance(location, item.XY) <= radius) { yield return item.ID; } } }
注:m_units.Items是指向T[]? m_data;的T[]属性,泛型类型为UnitData,该属性会在首次访问后将结果缓存到m_data数组中,避免重复计算。
锁的来源分析
锁的触发根源是**.NET对数组的自动线程安全检查**。因为结构体是值类型,遍历数组时会逐个拷贝结构体实例;而JIT编译器在多线程环境下,可能误判这种值类型拷贝操作存在并发风险,从而自动插入锁来保证数组元素访问的安全性——这个锁是CLR底层的隐式锁,不会显式出现在你的IL代码里。
当改为class后,数组存储的是引用类型,遍历仅拷贝引用,不存在值类型实例的拷贝操作,JIT不会触发这个隐式锁逻辑,所以锁竞争开销自然消失。
保留结构体的解决方案
- 直接访问底层数组:如果
m_data可以直接访问,跳过Items属性的缓存逻辑,直接遍历m_data数组,避免属性访问带来的额外检查触发锁。 - 将结构体改为只读结构体:把
UnitData标记为readonly struct,同时将XY的set改为init(或者直接用只读字段)。只读值类型的访问不存在写操作,JIT能判定无需线程安全锁,同时拷贝操作更轻量:public readonly struct UnitData { public int ID { get; init; } public Vector2 XY { get; init; } } - 用
Span<T>遍历数组:通过Span<UnitData>包装数组进行遍历,Span的访问方式更直接,JIT会优化掉不必要的锁检查:public IEnumerable<int> MoveNext(Vector2 location, float radius) { Span<UnitData> span = m_units.m_data.AsSpan(); foreach (ref readonly UnitData item in span) { if (Vector2.Distance(location, item.XY) <= radius) { yield return item.ID; } } } - 禁用数组大小检查(谨慎使用):在项目中添加编译符号
DISABLE_ARRAY_SZ_CHECK,可以关闭CLR对数组的一些安全检查,但前提是你能完全保证数组访问的线程安全和边界合法性。
内容的提问来源于stack exchange,提问作者user3797758
相关产品推荐
相关产品推荐

