理解 awaitable-awaiter 模式
Part1 - 【深入浅出C# async/await】编译篇
Part2 - 【深入浅出C# async/await】理解 awaitable-awaiter 模式
什么是awaitable
Part1中我们展示了任何Task都是可等待的。实际上还有其他可等待的类型。看下面的例子:
Task<int> task = new Task<int>(() => 0);
int result = await task.ConfigureAwait(false); //返回 ConfiguredTaskAwaitable<TResult>.
返回的 结构体 ConfiguredTaskAwaitable是可等待的,并且这完全不是Task:
public struct ConfiguredTaskAwaitable<TResult>
{
private readonly ConfiguredTaskAwaiter m_configuredTaskAwaiter;
internal ConfiguredTaskAwaitable(Task<TResult> task, bool continueOnCapturedContext)
{
this.m_configuredTaskAwaiter = new ConfiguredTaskAwaiter(task, continueOnCapturedContext);
}
public ConfiguredTaskAwaiter GetAwaiter()
{
return this.m_configuredTaskAwaiter;
}
}
它拥有一个GetAwaiter方法,实际上在Part1我们在Task中也看到了GetAwaiter()。
public class Task
{
public TaskAwaiter GetAwaiter()
{
return new TaskAwaiter(this);
}
}
public class Task<TResult> : Task
{
public new TaskAwaiter<TResult> GetAwaiter()
{
return new TaskAwaiter<TResult>(this);
}
}
另外一个例子是Task.Yield()
:
await Task.Yield(); // Returns a YieldAwaitable.
返回的YieldAwaitable也不是一个Task:
public struct YieldAwaitable
{
public YieldAwaiter GetAwaiter()
{
return default(YieldAwaiter);
}
}
同样,它只有一个GetAwaiter(),本文将主要探讨什么是可等待的。
awaitable - awaiter模式
观察不同的 awaitable/awaiter类型,我们可以发现,如果一个对象满足以下套件,那么它是是可等待的:
- 拥有一个GetAwaiter()方法(实例方法或者拓展方法都可)
- GetAwaiter()方法返回awaiter。当一个对象符合以下条件,那么它是一个awaiter:
- 继承了INotifyCompletion或者ICriticalNotifyCompletion接口;
- 拥有成员IsCompleted,IsCompleted有一个getter并且返回Boolean;
- 拥有GetResult(),返回void或者result;
这个 可等待/等待器模式 和 可迭代/迭代器模式 十分相似。下面是 可迭代/迭代器 接口的定义:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
//此处out T表示T只能作为方法返回类型,不能用于方法参数
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
T Current { get; }
}
"消失"的 IAwaitable和IAwaiter接口
和 IEnumarable 和 IEnumerator接口相同,awaitable/awaiter可以被可视化为IAwaitable/IAwaiter接口,这是非泛型版本:
public interface IAwaitable
{
IAwaiter GetAwaiter();
}
public interface IAwaiter : INotifyCompletion // or ICriticalNotifyCompletion
{
// INotifyCompletion has one method: void OnCompleted(Action continuation);
// ICriticalNotifyCompletion implements INotifyCompletion,
// also has this method: void UnsafeOnCompleted(Action continuation);
bool IsCompleted { get; } //Task执行完毕
void GetResult(); //Task执行完,获取结果,因为是非泛型结果,所以返回值为void
}
请注意GetResult()返回void,Task.GetAwaiter/TaskAwaiter.GetResult()符合这种情况。
下面是泛型版本:
public interface IAwaitable<out TResult>
{
IAwaiter<TResult> GetAwaiter();
}
public interface IAwaiter<out TResult> : INotifyCompletion // or ICriticalNotifyCompletion
{
bool IsCompleted { get; }
TResult GetResult();
}
这里唯一的区别就是GetResult()返回Result,Task.GetAwaiter()/TaskAwaiter.GetResult()符合这种情况。
请注意 .NET core实际上并没有定义IAwaitable/IAwaiter接口。IAwaitable接口会约束实例必须拥有GetAwaiter()方法。实际上C#支持GetAwaiter()实例方法和GetAwaiter拓展方法。
等待 Func/Action
在C#中我们并不能await并不能和lambda表达式一起用,下面的代码:
int result = await (() => 0);
编译器中会报错。
Cannot await ‘lambda expression’
这很好理解,因为lambda表达式(()=>0)可能是一个function或者一颗表达树。显然这里是function,我们可以这样告诉编译器这里的lambda的含义:
int result = await new Func<int>(()=>0);
它会报一个不同的错。
Cannot await ‘System.Func<int>’
ok,现在编译器的警告是关于类型而不是语法,理解了awaitable/awaiter模式,让Func变得可被等待并不是一件难事。
继承IAwaitable和IAwaiter接口实现GetAwaiter实例方法
首先,类似于上面的 ConfiguredTaskAwaitable,可以实现一个 FuncAwaitable 来包装 Func:
internal struct FuncAwaitable<TResult> : IAwaitable<TResult>
{
private readonly Func<TResult> function;
public FuncAwaitable(Func<TResult> function)
{
this.function = function;
}
public IAwaiter<TResult> GetAwaiter()
{
return new FuncAwaiter<TResult>(this.function);
}
}
FuncAwaitable 包装器用于实现 IAwaitable,因此它有一个实例方法 GetAwaiter(),该方法返回一个 IAwaiter,该 IAwaiter 也包装了 Func。FuncAwaiter 用于实现 IAwaiter:
public struct FuncAwaiter<TResult> : IAwaiter<TResult>
{
private readonly Task<TResult> task;
public FuncAwaiter(Func<TResult> function)
{
this.task = new Task<TResult>(function);
this.task.Start();
}
bool IAwaiter<TResult>.IsCompleted
{
get
{
return this.task.IsCompleted;
}
}
TResult IAwaiter<TResult>.GetResult()
{
return this.task.Result;
}
//continuation可以看Part1的what is callback部分
void INotifyCompletion.OnCompleted(Action continuation)
{
//await之后的代码作为continuation传入
//当前task执行完毕后,开启一个新task执行continuation
new Task(continuation).Start();
}
}
现在一个Func可以通过这种方式被等待。
int result = await new FuncAwaitable<int>(() => 0);
GetAwaiter()静态方法,不继承IAwaitable接口
正如 IAwaitable 所示,可等待对象只需要一个 GetAwaiter() 方法。在上面的代码中,FuncAwaitable 被创建为 Func 的包装器,并实现了 IAwaitable 接口,因此有一个 GetAwaiter() 实例方法。如果可以为 Func 定义 GetAwaiter() 扩展方法,则不再需要 FuncAwaitable:
public static class FuncExtensions
{
public static IAwaiter<TResult> GetAwaiter<TResult>(this Func<TResult> function)
{
return new FuncAwaiter<TResult>(function);
}
}
可以看到现在Func是可以直接被等待的。
int result = await new Func<int>(() => 0);
使用内置的awaiter awaitable: Task和TaskAwaiter
请记住,最常用的可等待程序/等待程序 - Task / TaskAwaiter。使用 Task / TaskAwaiter,就不再需要 FuncAwaitable / FuncAwaiter:
public static class FuncExtensions
{
public static TaskAwaiter<TResult> GetAwaiter<TResult>(this Func<TResult> function)
{
Task<TResult> task = new Task<TResult>(function);
task.Start();
return task.GetAwaiter(); // Returns a TaskAwaiter<TResult>.
}
}
同样地,Action的拓展方法:
public static class ActionExtensions
{
public static TaskAwaiter GetAwaiter(this Action action)
{
Task task = new Task(action);
task.Start();
return task.GetAwaiter(); // Returns a TaskAwaiter.
}
}
action也能被等待了:
await new Action(() => { }); //记得不能直接 await ()=>{} 编译器不知道这个lambda表达式是委托还是语法
现在任何function/action都能被等待了:
await new Action(() => HelperMethods.IO()); // or: await new Action(HelperMethods.IO);
如果委托有参数,会导致闭包:
int arg0 = 0;
int arg1 = 1;
int result = await new Action(() => HelperMethods.IO(arg0, arg1));
使用Task.Run()
上面的代码用于演示如何实现可等待程序/等待程序。由于等待函数/操作是常见的场景,.NET 提供了内置 API:Task.Run()。它们的实现方式类似于:
public class Task
{
public static Task Run(Action action)
{
// The implementation is similar to:
Task task = new Task(action);
task.Start();
return task; //task有GetAwaiter()方法,这就是为啥Task可以被await
}
public static Task<TResult> Run<TResult>(Func<TResult> function)
{
// The implementation is similar to:
Task<TResult> task = new Task<TResult>(function);
task.Start();
return task;
}
}
这是我们实际上等待function的方式:
int result = await Task.Run(() => HelperMethods.IO(arg0, arg1));
等待action:
await Task.Run(HelperMethods.IO);
Await IObservable
如果添加了 System.Reactive.Linq.dll 的引用(Rx(Reactive Extensions)的一部分),IObservable 和 IConnectableObservable 也可以变成可等待程序。在此库中,提供了 GetAwaiter() 扩展方法:
public static class Observable
{
public static AsyncSubject<TSource> GetAwaiter<TSource>(this IObservable<TSource> source);
public static AsyncSubject<TSource> GetAwaiter<TSource>(this IConnectableObservable<TSource> source);
}
所有返回AsyncSubject的方法是迭代器:(C#8.0中的AsyncSubject,下面的例子看看就好)
public sealed class AsyncSubject<T> : INotifyCompletion, ISubject<T>, ISubject<T, T>, IObserver<T>, IObservable<T>, IDisposable
{
public bool IsCompleted { get; }
public void OnCompleted();
// ...
}
所以它可以使用await关键字,举个IObservable的例子:
private static async Task AwaitObservable1()
{
IObservable<int> observable = Observable.Range(0, 3).Do(Console.WriteLine);
await observable;
}
输出
0
1
2
其他例子:
private static async Task<string> AwaitObservable2()
{
IObservable<string> observable = new string[]
{
"https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-1-compilation",
"https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-2-awaitable-awaiter-pattern",
"https://weblogs.asp.net/dixin/understanding-c-sharp-async-await-3-runtime-context",
}
.ToObservable<string>()
.SelectMany(async url => await new WebClient().DownloadStringTaskAsync(url))
.Select(StringExtensions.GetTitleFromHtml)
.Do(Console.WriteLine);
return await observable;
}
当GetTitleFromHtml如下:
public static string GetTitleFromHtml(this string html)
{
Match match = new Regex(
@".*<head>.*<title>(.*)</title>.*</head>.*",
RegexOptions.IgnoreCase | RegexOptions.Singleline).Match(html);
return match.Success ? match.Groups[1].Value : null;
}
运行上面的AwaitObservable2会输出每页的标题:
Dixin's Blog - Understanding C# async / await (1) Compilation
Dixin's Blog - Understanding C# async / await (3) Runtime Context
Dixin's Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern
确切地说他们是<title>和</title>之间的内容。