Ylisar 发表于 2022-6-3 18:19

Unity中的异步编程

在开发中经常会有异步的需求,比如等待一个操作的结束,又不影响后续同步逻辑的执行;下面就介绍下在unity开发中异步的处理方式
CPS

continuation-passing style的缩写,就是常见的回调方法;采用委托注册事件,来实现逻辑异步的效果
协程Coroutine

协程是unity利用枚举器配合引擎的gameloop来实现,协程的方式开发中应该经常用到,配合WaitForSeconds等unity内置的YieldInstruction,或者自定义的YieldInstruction,来实现异步的效果
为了和下面的async/await做对比,我们来写一个案例,代码如下:
using System.Collections;
using System.Linq;
using UnityEngine;

public class DataController : MonoBehaviour
{
    readonly string USERS_URL = "https://jsonplaceholder.typicode.com/users";
    readonly string TODOS_URL = "https://jsonplaceholder.typicode.com/todos";

    IEnumerator FetchData()
    {
      Todo[] todos;
      User[] users;
      
      // USERS
      var www = new WWW(USERS_URL);
      yield return www;
      if (!string.IsNullOrEmpty(www.error))
      {
            Debug.Log("An error occurred");
            yield break;
      }

      var json = www.text;
      try
      {
            var userRaws = JsonHelper.getJsonArray<UserRaw>(json);
            users = userRaws.Select(userRaw => new User(userRaw)).ToArray();
      }
      catch
      {
            Debug.Log("An error occurred");
            yield break;
      }

      // TODOS
      www = new WWW(TODOS_URL);
      yield return www;
      if (!string.IsNullOrEmpty(www.error))
      {
            Debug.Log("An error occurred");
            yield break;
      }

      json = www.text;
      try
      {
            var todoRaws = JsonHelper.getJsonArray<TodoRaw>(json);
            todos = todoRaws.Select(todoRaw => new Todo(todoRaw)).ToArray();
      }
      catch
      {
            Debug.Log("An error occurred");
            yield break;
      }

      // OUTPUT
      foreach (User user in users)
      {
            Debug.Log(user.Name);
      }

      foreach (Todo todo in todos)
      {
            Debug.Log(todo.Title);
      }
    }

    void Start()
    {
      StartCoroutine(FetchData());
    }
}
使用协程可以通过类似同步编程的方式来实现异步逻辑,但是存在一些弊端:
1、不能用try-catch来包裹yield,因此捕获异常需要手动来处理,并且内部的异常时,调试也无法看到调用堆栈
2、协程没有返回值,需要在协程内来处理异步获取的数据,导致协程代码段过大
3、获取user数据和todo数据是不同步的,先完成user数据拉取,成功后拉取todo
async/await

c#提供的异步编程能力,如果关键字无法使用,在unity中使用需要设置API为.net4.x。
这种方式是比较推荐使用的,先来看下上面使用协程的案例,使用async/await的实现方式
using System;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
public class DataAsyncController : MonoBehaviour
{
    readonly string USERS_URL = "https://jsonplaceholder.typicode.com/users";
    readonly string TODOS_URL = "https://jsonplaceholder.typicode.com/todos";
    async Task<User[]> FetchUsers()
    {
      var www = await new WWW(USERS_URL);
      if (!string.IsNullOrEmpty(www.error))
      {
            throw new Exception();
      }
      var json = www.text;
      var userRaws = JsonHelper.getJsonArray<UserRaw>(json);
      return userRaws.Select(userRaw => new User(userRaw)).ToArray();
    }
    async Task<Todo[]> FetchTodos()
    {
      var www = await new WWW(TODOS_URL);
      if (!string.IsNullOrEmpty(www.error))
      {
            throw new Exception();
      }
      var json = www.text;
      var todosRaws = JsonHelper.getJsonArray<TodoRaw>(json);
      return todosRaws.Select(todoRaw => new Todo(todoRaw)).ToArray();
    }
    async void Start()
    {
      try
      {
            var users = await FetchUsers();
            var todos = await FetchTodos();
            foreach (User user in users)
            {
                Debug.Log(user.Name);
            }
            foreach (Todo todo in todos)
            {
                Debug.Log(todo.Title);
            }
      }
      catch
      {
            Debug.Log("An error occurred");
      }
    }
}
结构比协程的方式要清晰很多,并且异步方法可以有返回值,并且可以直接await内置的异步api,这部分功能是可以自己扩展定义的,下面具体说下实现的原理。
async/await关键字会在编译时生成一个状态机类和一个异步方法处理类AsyncVoidMethodBuilder,类似处理流程的伪代码如下
var awaiter = task.GetAwaiter();
if (awaiter.IsCompleted)
{
    //等待结束直接返回异步结果,也可以没有返回值
    outcome = awaiter.GetResult();
}
else
{
    //挂起异步操作
    SuspendTheFunction();

    Action continuation = () => {
      //恢复
      ResumeTheFunction();
      // Remove 'outcome =' if `GetResult` returns void
      outcome = awaiter.GetResult();
    };

    var cnc = awaiter as ICriticalNotifyCompletion;
    if (cnc != null)
    {
      cnc.UnsafeOnCompleted(continuation);
    }
    else
    {
      awaiter.OnCompleted(continuation);
    }
}
为了更好的理解其原理,推荐看一下生成的IL代码,参考链接https://www.jacksondunstan.com/articles/4918
await后面的对象可以是task,也可以是自定义的类包含GetAwaiter()方法的实现,GetAwaiter方法会返回awaiter对象,该对象需要实现IsCompleted { get; }属性,OnCompleted(Action continuation)方法,GetResult()方法,具体接口如下
    interface IAwaitable<TResult>
    {
      IAwaiter<TResult> GetAwaiter();
    }

    interface IAwaiter<TResult>
    {
      bool IsCompleted { get; }
      void OnCompleted(Action continuation);
      TResult GetResult();
    }
不带返回值的可以定义为void,实现这两个接口就可以自定义awaiter,建议可以自己尝试下输出log,便于理解流程
unity的YieldInstruction适配await,推荐看下这个链接How to use Async-Await instead of coroutines in Unity3d 2017
具体的实现过程以WaitForSeconds为例,代码如下:
// GetAwaiter
// 适配WaitForSeconds类的GetAwaiter方法,通过GetAwaiterReturnVoid返回其Awaiter对象
public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSeconds instruction)
{
    return GetAwaiterReturnVoid(instruction);
}
// GetAwaiterReturnVoid
// 创建和返回Awaiter: SimpleCoroutineAwaiter
// 并在Unity主线程执行InstructionWrappers.ReturnVoid(awaiter, instruction)
static SimpleCoroutineAwaiter GetAwaiterReturnVoid(object instruction)
{
    var awaiter = new SimpleCoroutineAwaiter();
    RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine(
      InstructionWrappers.ReturnVoid(awaiter, instruction)));
    return awaiter;
}
// InstructionWrappers.ReturnVoid
// 这里其实已经在Unity主线程,所以这里本质是将await最终换回了yield,由Unity来驱动WaitForSeconds的完成
// 只不过yield完成之后,通过awaiter.Complete回到Awaiter.OnCompleted流程去
public static IEnumerator ReturnVoid(
            SimpleCoroutineAwaiter awaiter, object instruction)
{
    // For simple instructions we assume that they don't throw exceptions
    yield return instruction;
    awaiter.Complete(null);
}

// 确保Action在Unity主线程上运行
// SyncContextUtil.UnitySynchronizationContext在插件Install的时候就初始化好了
// 如果发现当前已经在Unity主线程,就直接执行Action,无需自己Post自己
static void RunOnUnityScheduler(Action action)
{
    if (SynchronizationContext.Current == SyncContextUtil.UnitySynchronizationContext)
    {
      action();
    }
    else
    {
      SyncContextUtil.UnitySynchronizationContext.Post(_ => action(), null);
    }
}

// 真正的Awaiter,它是无返回值的,对应还有一个SimpleCoroutineAwaiter<T>版本
// 它的实现比较简单,就是适配接口,记录委托回调(_continuation),并在Compele()任务完成时,通过RunOnUnityScheduler封送委托回调
public class SimpleCoroutineAwaiter : INotifyCompletion
{
    bool _isDone;
    Exception _exception;
    Action _continuation;

    public bool IsCompleted
    {
      get { return _isDone; }
    }

    public void GetResult()
    {
      Assert(_isDone);

      if (_exception != null)
      {
            ExceptionDispatchInfo.Capture(_exception).Throw();
      }
    }

    public void Complete(Exception e)
    {
      Assert(!_isDone);

      _isDone = true;
      _exception = e;

      // Always trigger the continuation on the unity thread when awaiting on unity yield
      // instructions
      if (_continuation != null)
      {
            RunOnUnityScheduler(_continuation);
      }
    }

    void INotifyCompletion.OnCompleted(Action continuation)
    {
      Assert(_continuation == null);
      Assert(!_isDone);

      _continuation = continuation;
    }
}
可以看到通过方法扩展,给waiteforseconds添加了GetAwaiter方法,然后自定义awaiter来控制异步的结束条件
参考链接:https://john-tucker.medium.com/unity-leveling-up-with-async-await-tasks-2a7971df9c57
https://wudaijun.com/2021/11/c-sharp-unity-async-programing/
最后推荐看下github上提供的工具库:https://github.com/Cysharp/UniTask 为unity开发的async/await工具库
页: [1]
查看完整版本: Unity中的异步编程