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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 11:57:44