C#线程锁同步

lock关键字

private objtct o = new object();
public float num_to_be_change = 1f;
public void Function1(float num){
        lock(o){
                num_to_be_change = num;
        }
}

锁定的是这个对象的实例,即在堆上的内容,所以当两个引用指向了同一个对象,使用lock关键字时都会发生线程同步

所以注意:

  • 不要使用临时变量(new object)锁定

  • 最好使用Private修饰被锁对象,以免在其他地方发生线程同步

静态锁与非静态锁

无论是静态还是非静态锁,lock语句均基于对象实例的内存地址实现同步。线程进入lock块时,会尝试独占该对象

在static方法中使用静态锁,可以到达类(class)锁的效果

public class MyClass
{
    private static readonly object _staticLock = new object();

    public static void StaticMethod()
    {
        lock (_staticLock)
        {
            // 所有实例共享此锁,全局同步
        }
    }
}

Monitor类

提供了更灵活的锁定机制。它允许尝试进入锁定状态(TryEnter),设置超时(Wait),以及在不需要时释放锁(Pulse和Exit),与lock的底层原理相同

private object _lockObj = new object();
void TryAccess()
{
    bool lockTaken = false;
    try
    {
        Monitor.TryEnter(_lockObj, 500, ref lockTaken); // 尝试获取锁,最多等待500ms
        if (lockTaken)
        {
            // 临界区代码
        }
    }
    finally
    {
        if (lockTaken)
            Monitor.Exit(_lockObj); // 确保释放锁
    }
}

其它锁

包括Mutex(跨进程锁)和Semaphore / SemaphoreSlim(信号量),但是在Unity当中的应用很少,有兴趣可以自己去看看,可以去参考:浅谈C之线程锁_c 线程锁-CSDN博客

补充:C#中的字符串常量池

这篇文章的起因是魔法学院项目中需要实现攻击打中敌人时只扣血一次(因为OnTriggerEnter()可能被触发多次),由于我的java寄术经历,自然而然想到了使用将每个攻击的唯一id:gameObject.GetInstanceID()封装为string然后将string.Intern()作为被锁对象,进行线程同步防止多次被判定,但是这种方法在Unity中是完全不可以的

讲讲字符串常量池

其实跟java差不多,不过java的常量池自从java8之后就在堆里面,而C#的常量池是独立于堆的

简单来说:

存储在常量池中:文本字符串常量、使用Intern()拼接

string s1 = "10";
string s2 = "20";
string s3 = string.Intern(s1 + s2);

存储在堆中:非文本字符串拼接、使用了new关键字

//常量池中
string s1 = "10";
string s2 = "20";
//堆中
string s3 = s1 + s2;
string s4 = new string(new char[]{'1','2'});

具体可参考文章C# string字符串内存管理深入分析(全程干货)_c# 字符串池-CSDN博客

Unity线程

为什么说Unity线程是不安全的

Unity 的底层引擎(如物理系统、渲染管线)是单线程的,主线程负责处理所有引擎内部状态

Unity 对象(如 GameObject、Transform)的内部状态没有设计为线程安全的,多线程同时修改会导致数据损坏。纯计算任务,如数学运算、算法处理、非 Unity 对象的数据处理以及网络请求之类的可以放在子线程中

协程怎么工作?

协程(Coroutine)完全运行在主线程中,其本质是一种基于迭代器的分帧异步编程模型,通过状态机实现代码的暂停和恢复

状态机

协程是 Unity 在主线程中实现的非抢占式多任务调度机制,所有协程代码都在主线程顺序执行。当你调用 StartCoroutine(IEnumerator) 时,Unity 会将协程包装为一个隐藏的状态机类,满足某个状态时通过 MoveNext() 方法逐步执行

迭代器

IEnumerator意为迭代器,协程通过 yield 关键字定义暂停点,Unity 引擎在每帧的特定阶段(如 Update 之后)恢复协程执行,其上下文就保存在生成的迭代器对象中,看看源码就知道了:

也是因此,相比于线程,协程的上下文切换开销会低很多

注意点:

  • 协程不能是多线程的代替,只是一种伪并行

  • 协程中的异常不会自动传播,需手动捕获

  • 长时间计算的协程会阻塞主线程,导致帧率下降

在unity中安全使用多线程

网上看到一种解决方法是将子线程的任务结果通过队列传递到主线程执行

public class MainThreadDispatcher : MonoBehaviour
{
    private static readonly Queue<Action> _actions = new Queue<Action>();
    private static readonly object _lock = new object();
    void Update()
    {
        lock (_lock)
        {
            while (_actions.Count > 0)
            {
                _actions.Dequeue().Invoke();
            }
        }
    }
    public static void RunOnMainThread(Action action)
    {
        lock (_lock)
        {
            _actions.Enqueue(action);
        }
    }
}
// 在子线程中调用
Task.Run(() =>
{
    // 子线程计算
    Vector3 result = HeavyCalculation();

    // 将结果提交到主线程
    MainThreadDispatcher.RunOnMainThread(() =>
    {
        transform.position = result; // 在主线程安全操作
    });
});

还有一种使用是Unity 的 Job System(没试过,等我以后试试看再写一篇