未分类

Unity插件ThreadNinjia源码分析——将任务分至子线程的协程

Spread the love

Unity并不支持.Net4.x的功能,所以async、Task等等新的并发抽象都没办法用,确实让我非常的烦恼。

我们都知道,Unity当中的协程是通过迭代器,将任务分至游戏主线程的每一帧当中,但是尽管如此,遇到cpu密集型的逻辑携程依旧会力不从心。这个时候我们常常就需要开一个新的线程来完成这些任务。当然自己开线程也是一种方法,今天要介绍的是一种可以将任务分至子线程中的协程。

我们看看如何使用这一代码插件。

这是插件自带的例子:

IEnumerator Blocking()
{
   LogAsync("Thread.Sleep(5000); -> See if cube rotates.");
   Thread.Sleep(5000);
   LogAsync("Jump to main thread.");
   yield return Ninja.JumpToUnity;
   LogSync("Thread.Sleep(5000); -> See if cube rotates.");
   yield return new WaitForSeconds(0.1f);
   Thread.Sleep(5000);
   LogSync("Jump to background.");
   yield return Ninja.JumpBack;
   LogAsync("Yield WaitForSeconds on background.");
   yield return new WaitForSeconds(3.0f);
}

在MonoBehavior的Start中调用协程。

IEnumerator Blocking()
void Start()
{
   this.StartCoroutineAsync(Blocking());
}
 
void Update()
{
    // rotate cube to see if main thread has been blocked;
    transform.Rotate(Vector3.up, Time.deltaTime * 180);
}

代码的意思很简单,首先将我们的协程放到主线程中,查看方块旋转是否被阻塞。
然后将协程放到另一条线程中,再看看线程是否被阻塞。

运行程序,我们可以直接看到,若放到主线程中,方块的旋转会被阻塞。而如果放在后台线程当中则不会。

这和cpu密集型的逻辑中经常发生的情况类似。

就像之前我们在为游戏做消息解析优化的时候发现需要将解析逻辑放到子线程是同一个道理。

那么现在我们来看一看源码。

我们先看一看StartCoroutineAsync是如何实现的。

public static class ThreadNinjaMonoBehaviourExtensions
{
    public static Coroutine StartCoroutineAsync(
        this MonoBehaviour behaviour, IEnumerator routine,
        out Task task)
    {
        task = new Task(routine);
        return behaviour.StartCoroutine(task);
    }
 
    public static Coroutine StartCoroutineAsync(
        this MonoBehaviour behaviour, IEnumerator routine)
    {
        Task t;
        return StartCoroutineAsync(behaviour, routine, out t);
    }
}

事实上,这一个插件的协程是自己实现了一个有状态的迭代器,根据每一个状态进行MoveNext。
等一下我们就会去看Task这一个类。

但是首先我们需要了解Task有哪些状态。

我们打开TaskState.cs
我们可以看到Task的各种状态。

namespace CielaSpike
{
 /// <summary>
 /// Running state of a task.
 /// </summary>
 public enum TaskState
 {
 /// <summary>
 /// Task has been created, but has not begun.Task初始化
 /// </summary>
 Init,
 /// <summary>
 /// Task is running.Task正在运行
 /// </summary>
 Running,
 /// <summary>
 /// Task has finished properly.Task完成
 /// </summary>
 Done,
 /// <summary>
 /// Task has been cancelled.Task取消
 /// </summary>
 Cancelled,
 /// <summary>
 /// Task terminated by errors.Task发生错误
 /// </summary>
 Error
 }
}

然后Task中所做的东西就显而易见了。根据不同的状态进行不同的MoveNext。

private bool OnMoveNext()
{
   // no running for null;
   if (_innerRoutine == null)
   return false;
 
   // set current to null so that Unity not get same yield value twice;
   Current = null;
 
   // loops until the inner routine yield something to Unity;
   while (true)
   {
      // a simple state machine;
      switch (_state)
      {
         // first, goto background;在一开始就进入后台进行并发操作
         case RunningState.Init:
            GotoState(RunningState.ToBackground);
            break;
         // running in background, wait a frame;
         // 如果是异步则直接跳过,因为任务在后台进行,不对主线程进行阻塞
         case RunningState.RunningAsync:
           return true;
 
         // runs on main thread;
         // 如果在主线程上运行,则直接运行MoveNextUnity函数进行同步操作,高cpu密集型操作将阻塞主线程
         case RunningState.RunningSync:
            MoveNextUnity();
            break;
 
         // need switch to background;
         // 切换到后台操作
         case RunningState.ToBackground:
            GotoState(RunningState.RunningAsync);
            // call the thread launcher;
            // 运行线程操作,将操作放入子线程当中
            MoveNextAsync();
            return true;
 
         // something was yield returned;
         // 遇到Yield标识位,如果标识位为JumpToBack则将运行模式切换到后台,如果为JumpToUnity则切换到主线程,其他yield就使用上一个标识符
         case RunningState.PendingYield:
            if (_pendingCurrent == Ninja.JumpBack)
            {
               // do not break the loop, switch to background;
               GotoState(RunningState.ToBackground);
            }
            else if (_pendingCurrent == Ninja.JumpToUnity)
            {
               // do not break the loop, switch to main thread;
               GotoState(RunningState.RunningSync);
            }
            else
            {
               // not from the Ninja, then Unity should get noticed,
               // Set to Current property to achieve this;
               Current = _pendingCurrent;
 
            // yield from background thread, or main thread?
            if (_previousState == RunningState.RunningAsync)
            {
               // if from background thread,
               // go back into background in the next loop;
               _pendingCurrent = Ninja.JumpBack;
            }
            else
            {
               // otherwise go back to main thread the next loop;
               _pendingCurrent = Ninja.JumpToUnity;
            }
 
            // end this iteration and Unity get noticed;
            return true;
            }
            break;
 
         // done running, pass false to Unity;
         //不管是完成、取消或者是其他的就直接结束迭代器
         case RunningState.Done:
         case RunningState.CancellationRequested:
         default:
            return false;
      }
   }
}

虽然我们没有看到整一个类,但是通过OnMoveNext函数,我们就可以对Task进行一个大概的运行过程的了解。
整体思路还是通过一个拥有状态的迭代器来进行不同的迭代操作。

不过需要注意的是,因为线程方法中使用的代码块是这样的:

 private void MoveNextAsync()
 {
    ThreadPool.QueueUserWorkItem(
    new WaitCallback(BackgroundRunner));
 }

Unity的许多游戏对象不允许在子线程中执行,所以这些东西还是乖乖放在主线程吧。
如果是文件解析、寻路计算、AI等等cpu计算则可以考虑放入子线程当中。
通过学习这款插件,我们可以了解到通过有状态的迭代器我们还是可以做许多事情的。
类似于ResetCore中的协程管理器,在单个协程中迭代词典中的多个协程,做到协程管理的目的。

ThreadNinja插件地址:https://www.assetstore.unity3d.com/cn/#!/content/15717

其中的更多源码可以直接把插件下下来看,因为是免费并且开源的。

注释也非常地详尽。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.