- 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 调用,需要把它编译为状态机
- 为什么一定要 async 关键字是为了避免存量代码存在一个叫 await 的变量,导致编译器出现歧义
- 所以 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 的对象需要实现哪些方法
所以
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 完成后可以根据自己的需要执行回调函数(多线程情况下会考虑是否回到原始线程,单线程情况下一般都是放到一个队列里等待轮询)
- 编译时,编译器自动生成一个