前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity应用架构设计(10)——绕不开的协程和多线程(Part 2)

Unity应用架构设计(10)——绕不开的协程和多线程(Part 2)

作者头像
用户1161731
发布2018-03-28 11:34:02
1.8K0
发布2018-03-28 11:34:02
举报
文章被收录于专栏:木宛城主木宛城主

在上一回合谈到,客户端应用程序的所有操作都在主线程上进行,所以一些比较耗时的操作可以在异步线程上去进行,充分利用CPU的性能来达到程序的最佳性能。对于Unity而言,又提供了另外一种『异步』的概念,就是协程(Coroutine),通过反编译,它本质上还是在主线程上的优化手段,并不属于真正的多线程(Thread)。那么问题来了,怎样在Unity中使用多线程呢?

Thread 初步认识

虽然这不是什么难点,但我觉得还是有必要提一下多线程编程几个值得注意的事项:

  • 线程启动

在Unity中创建一个异步线程是非常简单的,直接使用类System.Threading.Thread就可以创建一个线程,线程启动之后毕竟要帮我们去完成某件事情。在编程领域,这件事就可以描述了一个方法,所以需要在构造函数中传入一个方法的名称。

代码语言:javascript
复制
Worker workerObject = new Worker();
Thread workerThread = new Thread(workerObject.DoWork)
workerThread.Start();
  • 线程终止

线程启动很简单,那么线程终止呢,是不是调用Abort方法。不是,虽然Thread对象提供了Abort方法,但并不推荐使用它,因为它并不会马上停止,如果涉及非托管代码的调用,还需要等待非托管代码的处理结果。

一般停止线程的方法是为线程设定一个条件变量,在线程的执行方法里设定一个循环,并以这个变量为判断条件,如果为false则跳出循环,线程结束。

代码语言:javascript
复制
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不读缓存直接把最新的值返回。所以_shouldStopvolatile修饰。

Dispatcher的引入

是不是觉得多线程好简单,好像也没想象的那么复杂,当你愉快的在多线程中访问UI控件时,Duang~~~,一个错误告诉你,不能在异步线程访问UI控件。这是肯定的,跨线程访问UI控件是不安全的,理应被禁止。那怎么办呢?

如果你有其他客户端的开发经验,比如iOS或者WPF经验,肯定知道Dispatcher。Dispatcher翻译过来就是调度员的意思,简单理解就是每个线程都有唯一的调度员,那么主线程就有主线程的调度员,实际上我们的代码最终也是交给调度员去执行,所以要去访问UI线程上的控件,我们可以间接的向调度员发出命令。

所以在WPF中,跨线程访问UI控件一般的写法如下:

代码语言:javascript
复制
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

代码语言:javascript
复制
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就是一个消费者,每一帧都会执行,所以如果队列里有任务,它就执行

代码语言:javascript
复制
 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混合使用

到目前为止,相信你对CoroutineThread有清楚的认识,但它们并不是互斥的,可以混合使用,比如Coroutine等待异步线程返回结果,假设异步线程里执行的是非常复杂的AI操作,这显然放在主线程会非常繁重。

由于篇幅有限,我不贴完整代码了,只分析其中最核心思路: 在Thread中有一个WaitFor方法,它每一帧都会询问异步任务是否完成:

代码语言:javascript
复制
public bool Update(){
    if(_isDown){
        OnFinished ();
        return true;

    }
    return false;
}
public IEnumerator WaitFor(){
    while(!Update()){
        //暂停协同程序,下一帧再继续往下执行
        yield return null;
    }
}

那么在某一个UI线程中,等待异步线程的结果,注意利用StartCouroutine,此等待并非阻塞线程,相信你已经它内部的机制了。

代码语言:javascript
复制
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实际上就是IEnumeratoryield这两个语法糖让我们很难理解其中的奥秘,推荐使用反编译工具去查看,相信你会豁然开朗。 源代码托管在Github上,点击此了解

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Thread 初步认识
  • Dispatcher的引入
  • Coroutine和MultiThreading混合使用
  • 小结
相关产品与服务
代码托管
CODING 代码托管(CODING Code Repositories,CODING-CR)是为开发者打造的云端便捷代码管理工具,旨在为更多的开发者带去便捷、高效的开发体验,全面支持 Git/SVN 代码托管,包括代码评审、分支管理、超大仓库等功能。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档