当前位置:首页 > 技术 > 正文内容

C# Unity 中自定义 Task-Like 类型的 await/async 支持

访客 技术 2026年9月9日 11

本文探讨了 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 的要求:

  1. 必须有一个返回 bool 类型的 IsCompleted 属性。
  2. 必须有一个 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;
}
标签: C#Unityasync

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。