Unity 使用Async、Await语法实现异步线程
我们都知道Unity是单线程,哪怕后面高版本渲染这块移动到了其他线程。
Async、Await是C# 5.0的新加的语法,是个非常优雅的语法糖。
我们看看异步函数的声明:
async void SyncFunctionTest()
{
}
很简洁明了,就一个async关键字。虽然被标记异步关键字,但是,还是在当前线程执行。最后我们答应下线程的ID,证明我之前的说法是正确的,代码是这样的:
void Start()
{
Debug.Log(Thread.CurrentThread.ManagedThreadId);
SyncFunctionTest();
}
async void SyncFunctionTest()
{
Debug.Log(Thread.CurrentThread.ManagedThreadId);
}
我们看看控制台:
好了,进入主题,实现一个线程的切换!那么在Unity中我们如何优化的切换一个线程呢?线程池?new Thread? 不不不!
我们看下代码:
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
namespace EngineAsyncUtility
{
public partial class AsyncUtility : MonoBehaviour
{
public static AsyncUtility Instance;
private static int UnityThreadID { get; set; }
public static bool InUnityThreadContext => Thread.CurrentThread.ManagedThreadId == UnityThreadID;
public static SynchronizationContext UnitySyncContext { get; private set; }
public static SynchronizationContext BackgroundSyncContext { get; private set; }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
public static void AsyncUtilityInitlize()
{
UnityThreadID = Thread.CurrentThread.ManagedThreadId;
UnitySyncContext = SynchronizationContext.Current;
BackgroundSyncContext = new SynchronizationContext();
Instance = new GameObject("AsyncUtility").AddComponent<AsyncUtility>();
UnityEngine.Object.DontDestroyOnLoad(Instance);
}
}
}
SynchronizationContext ? 好陌生,这个是个什么鬼?用个通俗的大白话来说就是一个线程间上下文调度对象。那么它是如何完成一个线程间的上下文调度?我们看看源码:
/// <summary>When overridden in a derived class, dispatches a synchronous message to a synchronization context.</summary>
/// <param name="d">The <see cref="T:System.Threading.SendOrPostCallback" /> delegate to call.</param>
/// <param name="state">The object passed to the delegate.</param>
/// <exception cref="T:System.NotSupportedException">The method was called in a Windows Store app. The implementation of <see cref="T:System.Threading.SynchronizationContext" /> for Windows Store apps does not support the <see cref="M:System.Threading.SynchronizationContext.Send(System.Threading.SendOrPostCallback,System.Object)" /> method.</exception>
[__DynamicallyInvokable]
public virtual void Send(SendOrPostCallback d, object state)
{
d(state);
}
/// <summary>When overridden in a derived class, dispatches an asynchronous message to a synchronization context.</summary>
/// <param name="d">The <see cref="T:System.Threading.SendOrPostCallback" /> delegate to call.</param>
/// <param name="state">The object passed to the delegate.</param>
[__DynamicallyInvokable]
public virtual void Post(SendOrPostCallback d, object state)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(d.Invoke), state);
}
我只截取了部分代码,也是上下文调度的主要函数Send和Post,从明面上看,Send是在当前线程完成的调用,Post是从线程池取出来调用。
另外,最终被执行到Thread.cs中的GetExecutionContextReader函数中来切换上下文线程,(中间部分函数我没追踪到。)
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
internal ExecutionContext.Reader GetExecutionContextReader()
{
return new ExecutionContext.Reader(this.m_ExecutionContext);
}
其实最重要还是 send和post,你提交的方式决定了是否是在当前线程来提交。
AsyncUtility .cs大致就说到这里,那么我们如何使用Async和Await呢?
首先实现一个 SynchronizationContextAwaiter 有过Async和Await基本理解的都知道如果我们要自定义Async/Await,就要自行实现一个Awaiter并继承INotifyCompletion。
最后我实现的源码是这样的:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using UnityEngine;
namespace EngineAsyncUtility.Awaiters
{
public struct SynchronizationContextAwaiter : INotifyCompletion
{
private static readonly SendOrPostCallback _postCallback = state => ((Action)state)();
private readonly SynchronizationContext _context;
public SynchronizationContextAwaiter(SynchronizationContext context)
{
this._context = context;
}
public bool IsCompleted => _context == SynchronizationContext.Current;
public void OnCompleted(Action continuation)
{
_context.Post(_postCallback, continuation);
}
public void GetResult() { }
}
}
最后我们扩展下SynchronizationContext类:
namespace EngineAsyncUtility.Extensions
{
public static class Extensions
{
public static SynchronizationContextAwaiter GetAwaiter(this SynchronizationContext scontext)
=> new SynchronizationContextAwaiter(scontext);
}
}
OK!基本大致就是这样,非常简单!我们写个测试代码验证下结果:
void Start()
{
SyncContextTest();
}
async void SyncContextTest()
{
Debug.Log(AsyncUtility.InUnityThreadContext);
Debug.Log("Unity Thread true");
await AsyncUtility.BackgroundSyncContext;
Debug.Log(AsyncUtility.InUnityThreadContext);
Debug.Log("Unity Thread false");
await AsyncUtility.UnitySyncContext;
Debug.Log(AsyncUtility.InUnityThreadContext);
Debug.Log("Unity Thread true");
}
结果很明显了。
最后我们做个极端的测试,我们都知道,如果我们在其他线程操作UI,那么Unity就会给你throw一个error!我们试下是否真的切换到其他线程了!测试代码:
public Text t1;
public Text t2;
public Text t3;
void Start()
{
SyncContextTest();
}
async void SyncContextTest()
{
Debug.Log(AsyncUtility.InUnityThreadContext);
Debug.Log("Unity Thread true");
t1.text = "Unity Thread true";
await AsyncUtility.BackgroundSyncContext;
Debug.Log(AsyncUtility.InUnityThreadContext);
Debug.Log("Unity Thread false");
try
{
t2.text = "Unity Thread false";
}
catch (Exception e)
{
Debug.LogError(e);
}
await AsyncUtility.UnitySyncContext;
Debug.Log(AsyncUtility.InUnityThreadContext);
Debug.Log("Unity Thread true");
t3.text = "Unity Thread false";
}
干得漂亮小老弟,那么我们基于做个逻辑就可以实现很多种效果,包括Unity的协程。