C# Unity 中自定义 Task-Like 类型的 await/async 支持
本文探讨了 C# 中 async/await 关键字的底层机制,并展示了如何为自定义的类实现类似 Task 的可等待行为。这需要理解编译器在幕后进行的操作,并遵循特定的接口和方法约定。
1. 异步编程基础
在同步编程中,代码按顺序执行,一个操作完成后才开始下一个。然而,在异步编程中,async 方法可以在不阻塞主线程的情况下执行耗时操作。
以下是一个使用 async/await 的示例:
// 自动调用的方法
void Start()
{
PerformAsyncTask();
// 输出 "666" 到控制台
123.LogCustom();
}
// 异步执行的方法,每秒输出一个数字,不阻塞主线程
public async void PerformAsyncTask()
{
await CustomAsync.Delay(1);
1.LogCustom();
await CustomAsync.Delay(1);
2.LogCustom();
await CustomAsync.Delay(1);
3.LogCustom();
await CustomAsync.Delay(1);
4.LogCustom();
}
与同步逻辑不同,在上述异步示例中,"666" 会在 1, 2, 3, 4 之前输出。这是因为 PerformAsyncTask 方法在遇到 await 时不会暂停,而是继续执行后续代码,尽管它仍然在主线程上运行,但通过一种"伪多线程"的方式实现了非阻塞。
异步原理简析
1.Start 方法执行到 PerformAsyncTask。
2. 调用 AsyncTaskMethodBuilder.Create 创建一个构建器。
3. 进入 Delay 方法,获取其返回的 AsyncTask 对象。
4. await 等待 AsyncTask 时,调用 AsyncTask.GetAwaiter() 返回一个 IAwaiter 实例。
5. 调用 IAwaiter.OnCompleted 方法,将 await 之后的代码注册为一个 continuation。
6. Delay 方法内部的计时器完成后,会调用 AsyncTask.SetResult()。
7. 调用 SetResult 后,之前注册的 continuation(即 await 之后的代码)被执行。
8. 继续执行下一个 await。
总结:OnCompleted 中的 continuation 代表了 await 语句之后直到方法结束的所有代码。每次遇到 await,都会更新 continuation 以便执行下一段代码,前提是 IsCompleted 为 true,并且需要显式调用 SetResult 来切换状态。
2. 自定义 await 支持
要使自定义类能够被await 关键字支持,它必须遵循编译器的特定要求。最核心的要求是:类必须提供一个名为 GetAwaiter 的方法。
await new CustomAwaitableObject();
支持 await 关键字的唯一要求是:包含 GetAwaiter 方法 (也可以是扩展方法)。
await 关键字支持。
public interface IAwaitable<out TAwaiter> where TAwaiter : IAwaiter
{
TAwaiter GetAwaiter();
}
public interface IAwaitable<out TAwaiter, out TResult> where TAwaiter : IAwaiter<TResult>
{
TAwaiter GetAwaiter();
}
GetAwaiter 方法返回一个 IAwaiter 实例。接下来,我们将自定义 Awaiter。
3. 自定义 Awaiter
System.Runtime.CompilerServices 命名空间提供了 INotifyCompletion 和 ICriticalNotifyCompletion 接口,编译器要求 Awaiter 必须实现其中之一。
using System.Runtime.CompilerServices;
public interface INotifyCompletion
{
void OnCompleted(Action continuation);
}
public interface ICriticalNotifyCompletion : INotifyCompletion
{
void UnsafeOnCompleted(Action continuation);
}
Awaiter 的要求:
- 必须有一个返回
bool类型的IsCompleted属性。 - 必须有一个
GetResult方法。
IsCompleted 属性在遇到 await 时会被检查。如果为 true,则表示异步操作已完成,await 语句会立即结束。
int value = await GetValueAsync();
GetResult 方法的含义也很直观。当 await 操作完成后,它会被调用以返回结果。
public interface IAwaiter : INotifyCompletion
{
bool IsCompleted { get; }
void GetResult();
}
public interface IAwaiter<T> : INotifyCompletion
{
bool IsCompleted { get; }
T GetResult();
}
public interface ICriticalAwaiter : ICriticalNotifyCompletion, IAwaiter
{
}
public interface ICriticalAwaiter<T> : ICriticalNotifyCompletion, IAwaiter<T>
{
}
这里定义了四个与 Awaiter 相关的接口。CriticalAwaiter 相较于普通 Awaiter,多了一个 UnsafeOnCompleted 方法,这在处理可能产生负面影响(如异常)的异步代码时很有用。
3. 自定义异步任务接口
为了给异步任务提供更多功能,我们可以进一步扩展。这些扩展并非编译器强制要求,而是根据自身需求。当任务完成时,我们可以手动调用SetResult,出现异常时调用 SetException。
public interface IAsyncTask : ICriticalAwaiter
{
void SetResult();
void SetException(Exception exception);
}
public interface IAsyncTask<T> : ICriticalAwaiter<T>
{
// 参数即为 GetResult() 将要返回的结果
void SetResult(T result);
void SetException(Exception exception);
}
4. 实现异步任务类
实现自定义异步任务类时,务必加上[AsyncMethodBuilder] 特性。这会告知编译器在返回该类型时自动创建 AsyncTaskMethodBuilder。对于泛型类型,应使用 [AsyncMethodBuilder(typeof(AsyncTaskMethodBuilder<>))]。
using System.Runtime.CompilerServices;
using System;
// 必须加上此特性,指定 AsyncTaskMethodBuilder
[AsyncMethodBuilder(typeof(AsyncTaskMethodBuilder))]
public partial class AsyncTask : PoolableObject<AsyncTask>, IAsyncTask // 假设 PoolableObject 和 IAsyncTask 已定义
{
public AsyncTask GetAwaiter() => this;
private Action _continuation; // 用于存储 await 后续需要执行的代码
public Exception Exception { get; private set; }
public bool IsCompleted { get; set; }
public void OnCompleted(Action continuation)
{
// 实际调用 UnsafeOnCompleted,因为我们实现了 ICriticalNotifyCompletion
UnsafeOnCompleted(continuation);
}
public void UnsafeOnCompleted(Action continuation)
{
_continuation = continuation;
}
public void SetException(Exception exception)
{
IsCompleted = true;
Exception = exception;
// 异常发生时,也可能需要执行 continuation,取决于具体场景
// _continuation?.Invoke();
}
// 无返回值任务的 GetResult
public void GetResult()
{
// 如果有异常,在此处抛出
if (Exception != null)
{
throw Exception;
}
}
// 无返回值任务的 SetResult
public void SetResult()
{
IsCompleted = true;
// 执行 await 后续的代码
_continuation?.Invoke();
// 释放对象到对象池(假设 PoolableObject 提供了 Dispose 方法)
this.Dispose();
}
}
5. AsyncTaskMethodBuilder
这是编译器要求的另一个部分,它充当了异步状态机的构建器。
using System.Runtime.CompilerServices;
using System;
using System.Diagnostics;
public struct AsyncTaskMethodBuilder
{
private AsyncTask _task;
// 1. Create: 编译器调用此静态方法创建 Builder
[DebuggerHidden]
public static AsyncTaskMethodBuilder Create()
{
// 从对象池创建 AsyncTask 实例
return new AsyncTaskMethodBuilder(AsyncTask.Create(fromPool: true));
}
// 2. 构造器: 初始化 Builder
private AsyncTaskMethodBuilder(AsyncTask task)
{
_task = task;
}
// 3. Task: 提供对 AsyncTask 实例的访问
[DebuggerHidden]
public AsyncTask Task => _task;
// 4. Start: 启动状态机
[DebuggerHidden]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
stateMachine.MoveNext();
}
// 5. SetException: 处理异步操作中的异常
[DebuggerHidden]
public void SetException(Exception exception)
{
_task.SetException(exception);
}
// 6. SetResult: 标记任务成功完成
[DebuggerHidden]
public void SetResult()
{
_task.SetResult();
}
// 7. AwaitOnCompleted: 处理需要通知的 awaiter
[DebuggerHidden]
public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
{
// 注册 continuation,当 awaiter 完成时调用状态机的 MoveNext
awaiter.OnCompleted(stateMachine.MoveNext);
}
// 8. AwaitUnsafeOnCompleted: 处理临界 awaiter
[SecuritySafeCritical]
public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
// 注册 continuation,当 awaiter 完成时调用状态机的 MoveNext
awaiter.UnsafeOnCompleted(stateMachine.MoveNext);
}
// 9. SetStateMachine: 关联状态机
[DebuggerHidden]
public void SetStateMachine(IAsyncStateMachine stateMachine)
{
// 通常不需要在此处做特别处理,除非需要管理状态机实例
}
}
关于 OnCompleted 参数委托的垃圾回收问题,编译器在编译时会将代码转换为状态机,因此 continuation 实际上是静态编译的一部分,不会产生额外的 GC 开销。
6. Delay 方法示例
下面是一个使用自定义AsyncTask 实现的 Delay 方法示例。该实现从对象池中获取对象,避免了 GC 压力。相比之下,.NET 原生的 Task 类型通常更为庞大且可能带来更高的 GC 开销。
using System;
using System.Threading; // 假设 TimerCall 使用了 Threading
public static class CustomAsync
{
// 假设 Framework.Pool 是一个对象池管理器
public static AsyncTask Delay(float seconds)
{
var task = Framework.Pool.Allocate<AsyncTask>();
var timer = Framework.Pool.Allocate<TimerCall>(); // 假设 TimerCall 是一个自定义的计时器类
// 当计时器完成时,设置 AsyncTask 的结果,并释放计时器
Action timerCallback = () =>
{
task.SetResult();
Framework.Pool.Free(timer); // 假设 Pool.Free 用于释放对象
};
timer.OnCompleted += timerCallback;
timer.Start(TimeSpan.FromSeconds(seconds));
return task;
}
}
// 假设的 TimerCall 类
public class TimerCall : IDisposable
{
public event Action OnCompleted;
private Timer _timer;
public void Start(TimeSpan duration)
{
_timer = new Timer(_ => OnCompleted?.Invoke(), null, duration, Timeout.InfiniteTimeSpan);
}
public void Dispose()
{
_timer?.Dispose();
_timer = null;
// 标记为已释放,防止重复调用
GC.SuppressFinalize(this);
}
// Finalizer (可选,如果需要确保 Dispose 被调用)
~TimerCall()
{
Dispose();
}
}
// 假设的 PoolableObject 和 Pool 类
public abstract class PoolableObject<T> : IDisposable where T : PoolableObject<T>
{
public virtual void Dispose() { /* ... */ }
public static T Create(bool fromPool) { /* ... */ return default(T); }
}
public static class Framework
{
public static class Pool
{
public static T Allocate<T>(bool fromPool = true) where T : PoolableObject<T>, new() => new T(); // 简化示例
public static void Free<T>(T obj) where T : PoolableObject<T> { obj.Dispose(); }
}
}
C# 3.0 反编译 System.Tasks.Task 的 IL 代码
以下是通过 ILSpy 反编译的示例代码及其 IL 代码,以展示async/await 的编译器转换过程。
被反编译的代码:
public async Task TaskTest()
{
await Task.Run(() => { });
}
反编译得到的 IL 代码(简化):
// 编译器生成的异步状态机类
[CompilerGenerated]
private sealed class <TaskTest>d__1 : IAsyncStateMachine
{
public int <>1__state; // 状态机当前状态
public AsyncTaskMethodBuilder <>t__builder; // 异步方法构建器
public ClassA <>4__this; // 对类的实例引用 (如果方法是非静态的)
private TaskAwaiter <>u__1; // 用于 await Task.Run 的 TaskAwaiter
// MoveNext 方法是状态机的核心,处理状态转移
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter;
if (num != 0) // 检查当前状态
{
// 第一次进入或从暂停处恢复
awaiter = Task.Run(delegate { }).GetAwaiter(); // 执行 Task.Run 并获取 awaiter
if (!awaiter.IsCompleted) // 如果未完成
{
// 记录当前状态和 awaiter,然后挂起
<>1__state = 0;
<>u__1 = awaiter;
<TaskTest>d__1 stateMachine = this;
// 调用 builder 的 AwaitUnsafeOnCompleted 来注册 continuation
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return; // 退出 MoveNext,等待继续
}
}
else
{
// 从暂停处恢复,之前的 awaiter 已经完成
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter); // 清理已使用的 awaiter
<>1__state = -1; // 标记为已完成或准备完成
}
awaiter.GetResult(); // 获取 await 的结果 (这里不需要实际结果)
}
catch (Exception exception) // 异常处理
{
<>1__state = -2; // 标记为异常结束
<>t__builder.SetException(exception); // 将异常设置到 builder
return;
}
// 正常完成
<>1__state = -2; // 标记为最终状态
<>t__builder.SetResult(); // 调用 builder 的 SetResult
}
// 实现 IAsyncStateMachine 接口
void IAsyncStateMachine.MoveNext()
{
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine) { }
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
this.SetStateMachine(stateMachine);
}
}
// 异步方法的实际实现
[AsyncStateMachine(typeof(<TaskTest>d__1))] // 关联状态机类
[DebuggerStepThrough]
public Task TaskTest()
{
// 创建状态机实例
<TaskTest>d__1 stateMachine = new <TaskTest>d__1();
// 初始化状态机成员
stateMachine.<>4__this = this; // 如果是实例方法
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create(); // 创建 Builder
stateMachine.<>1__state = -1; // 设置初始状态
// 获取 Builder
AsyncTaskMethodBuilder builder = stateMachine.<>t__builder;
// 启动状态机
builder.Start(ref stateMachine);
// 返回 builder 关联的 Task (这里是 AsyncTask)
return stateMachine.<>t__builder.Task;
}