|
在开发中经常会有异步的需求,比如等待一个操作的结束,又不影响后续同步逻辑的执行;下面就介绍下在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(&#34;An error occurred&#34;);
yield break;
}
// TODOS
www = new WWW(TODOS_URL);
yield return www;
if (!string.IsNullOrEmpty(www.error))
{
Debug.Log(&#34;An error occurred&#34;);
yield break;
}
json = www.text;
try
{
var todoRaws = JsonHelper.getJsonArray<TodoRaw>(json);
todos = todoRaws.Select(todoRaw => new Todo(todoRaw)).ToArray();
}
catch
{
Debug.Log(&#34;An error occurred&#34;);
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 = &#34;https://jsonplaceholder.typicode.com/users&#34;;
readonly string TODOS_URL = &#34;https://jsonplaceholder.typicode.com/todos&#34;;
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(&#34;An error occurred&#34;);
}
}
}
结构比协程的方式要清晰很多,并且异步方法可以有返回值,并且可以直接await内置的异步api,这部分功能是可以自己扩展定义的,下面具体说下实现的原理。
async/await关键字会在编译时生成一个状态机类和一个异步方法处理类AsyncVoidMethodBuilder,类似处理流程的伪代码如下
var awaiter = task.GetAwaiter();
if (awaiter.IsCompleted)
{
//等待结束直接返回异步结果,也可以没有返回值
outcome = awaiter.GetResult();
}
else
{
//挂起异步操作
SuspendTheFunction();
Action continuation = () => {
//恢复
ResumeTheFunction();
// Remove &#39;outcome =&#39; 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&#39;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工具库 |
|