网站首页 文章专栏 深入理解 await 编程范式
深入理解 await 编程范式
发布 作者:被打断de狗腿 浏览量:54
- async 关键字只是告诉编译器,这个函数内部可以有 await 调用,需要把它编译为状态机 - await 的对象既不是函数,也不是Task,而是一个符合 `IAwaitable` 接口描述的鸭子类型(实际上并没有这个接口存在) 不直接用接口的好处是,只用拓展方法就可以把一个对象拓展成 awaitable 对象,对代码没有侵入性

深入理解 await 编程范式

用法示例写在前面

public class Program
{
    private static async Task Main(string[] args)
    {
        // 异步发起一个网络请求
        var http = new HttpClient();
        var txt = await http.GetStringAsync("https://www.bing.com");
        Console.WriteLine(txt);
    }
}

关键字说明

  • async 关键字只是告诉编译器,这个函数内部可以有 await 调用,需要把它编译为状态机

  • await 的对象既不是函数,也不是Task,而是一个符合 IAwaitable 接口描述的鸭子类型(实际上并没有这个接口存在)
    不直接用接口的好处是,只用拓展方法就可以把一个对象拓展成 awaitable 对象,对代码没有侵入性

    public interface IAwaitable<T>
    {
        IAwaiter<T> GetAwaiter();
    }
    public interface IAwaiter<T>
    {
        bool IsCompleted;
        void OnCompleted(Action continuation);
        T GetResult();
    }
    

    在 Rider 中可以看到 await 的对象需要实现哪些方法

    在 Rider 中可以看到 await 的对象需要实现哪些方法

    所以 async void MethodAsync() { } 是不能被 await 的,因为它返回的对象是 void

对 await 编程范式的理解

  • C# 标准库中常见的是对 Task 的拓展,由于Task与多线程、线程池的关系过于紧密,很多人会误以为 await 就是多线程。但实际上只是多线程编程刚好符合 await 这个语法糖的编程范式。
  • 任何需要等待的操作都可以改造成适配 await 语法糖的模式
  • 常见的需要等待的操作有多线程、网络IO、文件IO。不常见的,等待一个按钮被点击、等待一个窗口被打开,也可以改造成 await 的编程模式
  • await 关键字的对象本质不是 Task、UniTask,而是前面说的鸭子类型,任何对象都可以改造成这样的鸭子类型(事件、Button、窗口的打开与关闭)

把 Button 的点击操作拓展成 awaitable 对象

public class ButtonAwaiter : INotifyCompletion
{
    public bool IsCompleted { get; private set; }

    private Button m_Btn;
    private Action m_Continuation;

    public ButtonAwaiter(Button btn)
    {
        m_Btn = btn;
    }

    public void OnCompleted(Action continuation)
    {
        m_Continuation = continuation;
        m_Btn.onClick.AddListener(OnClickInternal);
    }

    public void GetResult() { }

    private void OnClickInternal()
    {
        m_Btn.onClick.RemoveListener(OnClickInternal);

        m_Continuation?.Invoke();
        m_Continuation = null;
        IsCompleted = true;
    }
}

public static class ButtonEx
{
    public static ButtonAwaiter GetAwaiter(this Button self) => new ButtonAwaiter(self);
}

// 拓展后使用方式
private async void Start()
{
    var btn = GetComponent<Button>();
    await btn;
    Debug.Log("Button Click");
}

await 关键字做了什么?

  • await 关键字是把原本需要程序员手动编写的注册回调函数等操作,通过编译时生成代码的方式自动完成了(所以都说 await 只是语法糖)
  • 如果是多线程编程,回调函数会牵涉到是否回到原来的主线程的问题,很多介绍 await 关键字的文章对这块大书特书,我觉得有点本末倒置
    • 不是觉得多线程编程不重要,而是把多线程编程与 await 编程范式混为一谈会增加读者理解成本
  • 这里不详细说明代码,只介绍运行思路:
    • 编译时,编译器自动生成一个 IAsyncStateMachine 对象(这个对象本身不可定制,debug编译时这个对象是class,release编译这个对象是struct)
    • 这个对象的第一次MoveNext() 是获取 awaiter 对象,并且通过 AsyncTaskMethodBuilder 把 MoveNext() 方法注册为 awaiter 完成时的回调
      • awaiter对象的获取交由用户自定义,它可以是一个函数返回的 Task(比如发起网络IO后返回 Task),也可以是一个Button对象本身(监听Button的点击事件)
      • 对一个awaitable对象添加 [AsyncMethodBuilder] 属性,可以指定它的 builder
    • awaiter 完成后可以根据自己的需要执行回调函数(多线程情况下会考虑是否回到原始线程,单线程情况下一般都是放到一个队列里等待轮询)
loading