Unity应用架构设计:绕不开的协程和多线程
Coroutine
),通过反编译,它本质上还是在主线程上的优化手段,并不属于真正的多线程(Thread
)。那么问题来了,怎样在Unity中使用多线程呢?Thread 初步认识
虽然这不是什么难点,但我觉得还是有必要提一下多线程编程几个值得注意的事项:
-
线程启动
在Unity中创建一个异步线程是非常简单的,直接使用类System.Threading.Thread
就可以创建一个线程,线程启动之后毕竟要帮我们去完成某件事情。在编程领域,这件事就可以描述了一个方法,所以需要在构造函数中传入一个方法的名称。
1 | Worker workerObject = new Worker();Thread workerThread = new Thread(workerObject.DoWork)workerThread.Start(); |
-
线程终止
线程启动很简单,那么线程终止呢,是不是调用Abort
方法。不是,虽然Thread
对象提供了Abort
方法,但并不推荐使用它,因为它并不会马上停止,如果涉及非托管代码的调用,还需要等待非托管代码的处理结果。
一般停止线程的方法是为线程设定一个条件变量,在线程的执行方法里设定一个循环,并以这个变量为判断条件,如果为
false
则跳出循环,线程结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Worker { public void DoWork() { while (!_shouldStop) { Console.WriteLine( "worker thread: working..." ); } Console.WriteLine( "worker thread: terminating gracefully." ); } public void RequestStop() { _shouldStop = true ; } private volatile bool _shouldStop; } |
所以,你可以在应用程序退出(OnApplicationQuit
)时,将_shouldStop
设置为true
来到达线程的安全退出。
-
共享数据处理
多线程最麻烦的一点就是共享数据的处理了,想象一下A,B两个线程同一时刻处理一个变量,它最终的值到底是什么。所以一般需要使用lock
,但C#提供了另一个关键字volatile
,告诉CPU不读缓存直接把最新的值返回。所以_shouldStop
被volatile
修饰。
Dispatcher的引入
是不是觉得多线程好简单,好像也没想象的那么复杂,当你愉快的在多线程中访问UI控件时,Duang~~~,一个错误告诉你,不能在异步线程访问UI控件。这是肯定的,跨线程访问UI控件是不安全的,理应被禁止。那怎么办呢?
如果你有其他客户端的开发经验,比如iOS或者WPF经验,肯定知道Dispatcher。Dispatcher翻译过来就是调度员的意思,简单理解就是每个线程都有唯一的调度员,那么主线程就有主线程的调度员,实际上我们的代码最终也是交给调度员去执行,所以要去访问UI线程上的控件,我们可以间接的向调度员发出命令。
所以在WPF中,跨线程访问UI控件一般的写法如下:
1 2 3 4 5 6 7 | Thread thread= new Thread(()=>{ this .Dispatcher.Invoke(()=>{ //UI this .textBox.text=... this .progressBar.value=... }); }); |
嗯~ o( ̄▽ ̄)o,不错,但尴尬的是Unity没有提供Dispatcher啊!
对,但我们可以自己实现,把握住几个关键点:
-
自己的Dispatcher一定是一个MonoBehaviour,因为访问UI控件需要在主线程上
-
什么时候去更新呢,考虑生产者-消费者模式,有任务来了,我就是更新到UI上
-
在Unity中有这么个方法可以轮询是不是有任务要更新,那就是
Update
方法,每一帧会执行
所以自定义的UnityDispatcher
提供一个BeginInvoke
方法,并接送一个Action
1 2 3 4 5 6 7 8 9 10 11 12 13 | public void BeginInvoke(Action action){ while ( true ) { //以原子操作的形式,将 32 位有符号整数设置为指定的值并返回原始值。 if (0 == Interlocked.Exchange ( ref _lock, 1)) { //acquire lock _wait.Enqueue(action); _run = true ; //exist Interlocked.Exchange ( ref _lock,0); break ; } } } |
这是一个生产者,向队列里添加需要处理的Action。有了生产者之后,还需要消费者,Unity中的Update
就是一个消费者,每一帧都会执行,所以如果队列里有任务,它就执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | void Update(){ if (_run) { Queue<Action> execute = null ; //主线程不推荐使用lock关键字,防止block 线程,以至于deadlock if (0 == Interlocked.Exchange ( ref _lock, 1)) { execute = new Queue<Action>(_wait.Count); while (_wait.Count!=0){ Action action = _wait.Dequeue (); execute.Enqueue (action); } //finished _run= false ; //release Interlocked.Exchange ( ref _lock,0); } //not block if (execute != null ) { while (execute.Count != 0) { Action action = execute.Dequeue (); action (); } } } } |
值得注意的是,Queue
不是线程安全的,所以需要锁,我使用了Interlocked.Exchange
,好处是它以原子的操作来执行并且还不会阻塞线程,因为主线程本身任务繁重,所以我不推荐使用lock
。
Coroutine和MultiThreading混合使用
到目前为止,相信你对Coroutine
和Thread
有清楚的认识,但它们并不是互斥的,可以混合使用,比如Coroutine
等待异步线程返回结果,假设异步线程里执行的是非常复杂的AI操作,这显然放在主线程会非常繁重。
由于篇幅有限,我不贴完整代码了,只分析其中最核心思路:
在Thread
中有一个WaitFor
方法,它每一帧都会询问异步任务是否完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public bool Update(){ if (_isDown){ OnFinished (); return true ; } return false ; } public IEnumerator WaitFor(){ while (!Update()){ //暂停协同程序,下一帧再继续往下执行 yield return null ; } } |
那么在某一个UI线程中,等待异步线程的结果,注意利用StartCouroutine
,此等待并非阻塞线程,相信你已经它内部的机制了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | void Start(){ Debug.Log( "Main Thread :" +Thread.CurrentThread.ManagedThreadId+ " work!" ); StartCoroutine (Move()); } IEnumerator Move() { pinkRect.transform.DOLocalMoveX(250, 1.0f); yield return new WaitForSeconds(1); pinkRect.transform.DOLocalMoveY(-150, 2); yield return new WaitForSeconds(2); //AI操作,陷入深思,在异步线程执行,GreenRect不会卡顿 job.Start(); yield return StartCoroutine (job.WaitFor()); pinkRect.transform.DOLocalMoveY(150, 2); } |
小结
这两篇文章为大家介绍了怎样在Unity中使用协程和多线程,多线程其实不难,但同步数据是最麻烦的。Coroutine实际上就是IEnumerator
和yield
这两个语法糖让我们很难理解其中的奥秘,推荐使用反编译工具去查看,相信你会豁然开朗。
作者:哆啦的时光机
链接:https://www.imooc.com/article/26181/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!