本文手动翻译自:Extending the async methods in C#
上一篇文章中,我们讨论了 C# 编译器是如何处理异步方法的。在这一篇,我们会重点关注 C# 编译器为自定义异步方法提供的可扩展性。
我们有三种控制异步方法状态机的思路:
-
在
System.Runtime.CompilerServices
命名空间中提供自己的异步方法builder
。 -
自定义
task awaiters
。 -
自定义
task-like
类。
在 System.Runtime.ComplierServices
命名空间下自定义 AsyncMethodBuilder
类
从前文我们得知,C# 编译器会依据一些系统提供的类将异步方法转换为状态机。但 C# 编译器并不要求所依据的类来自指定的某个程序集。例如,我们可以在项目中提供自己的 ,这样 C# 编译器会将异步机制与该类“绑定”。
下面的例子将帮助我们理解底层转换,并看看代码运行时发生的事情:
namespace System.Runtime.CompilerServices
{
// AsyncVoidMethodBuilder.cs in your project
public class AsyncVoidMethodBuilder
{
public AsyncVoidMethodBuilder()
=> Console.WriteLine(".ctor");
public static AsyncVoidMethodBuilder Create()
=> new AsyncVoidMethodBuilder();
public void SetResult() => Console.WriteLine("SetResult");
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{
Console.WriteLine("Start");
stateMachine.MoveNext();
}
// AwaitOnCompleted, AwaitUnsafeOnCompleted, SetException
// and SetStateMachine are empty
}
}
AsyncVoidMethodBuilder
[Test]
public void RunAsyncVoid()
{
Console.WriteLine("Before VoidAsync");
VoidAsync();
Console.WriteLine("After VoidAsync");
async void VoidAsync() { }
}
该测试的输出:
Before VoidAsync
.ctor
Start
SetResult
After VoidAsync
UnsafeAwaitOnComplete
方法,来试验当异步方法返回还没完成任务的 Task
对象的时候的情况。完整的例子可以在 找到。
要修改 async Task
和 async Task<T>
方法的行为,你需要提供自定义版本的 和 。
关于上面提到的类,可以在 github 上的 项目里找到参考: 和 。
感谢 在这项目上给予我(作者)的灵感。
自定义 awaiters
前面的例子有亿点 “黑魔法” 的味道,并且它不适合用在真正的生产环境里。我们可以通过它学习异步机制,但你肯定不想看到这些代码出现在自己的项目里。好在 C# 作者给编译器内置了可扩展的点,允许在异步方法中 await
不同的对象类型。
可被 await
的类(即在 await
表达式中合法)需要满足以下几个条件:
-
该类中需要有名为
GetAwaiter
的方法(扩展方法也可以),否则编译器会报错。该方法的返回值也有两处限制: -
被返回的对象需要实现 接口。
-
被返回的对象需要有
bool IsCompleted {get;}
属性,和GetResult()
方法。
知道这些以后,我们可以写出可等待的 Lazy<T>
类:
public struct LazyAwaiter<T> : INotifyCompletion
{
private readonly Lazy<T> _lazy;
public LazyAwaiter(Lazy<T> lazy) => _lazy = lazy;
public T GetResult() => _lazy.Value;
public bool IsCompleted => true;
public void OnCompleted(Action continuation) { }
}
public static class LazyAwaiterExtensions
{
public static LazyAwaiter<T> GetAwaiter<T>(this Lazy<T> lazy)
{
return new LazyAwaiter<T>(lazy);
}
}
public static async Task Foo()
{
var lazy = new Lazy<int>(() => 42);
var result = await lazy;
Console.WriteLine(result);
}
项目就提供了 来在异步方法中等待 IObservable<T>
对象。基础类库(BCL)自己也提供 给 和
public struct HopToThreadPoolAwaitable : INotifyCompletion
{
public HopToThreadPoolAwaitable GetAwaiter() => this;
public bool IsCompleted => false;
public void OnCompleted(Action continuation) => Task.Run(continuation);
public void GetResult() { }
}
HotToThreadPoolAwaitable
[Test]
public async Task Test()
{
var testThreadId = Thread.CurrentThread.ManagedThreadId;
await Sample();
async Task Sample()
{
Assert.AreEqual(Thread.CurrentThread.ManagedThreadId, testThreadId);
await default(HopToThreadPoolAwaitable);
Assert.AreNotEqual(Thread.CurrentThread.ManagedThreadId, testThreadId);
}
}
await
关键字之前的部分)都已同步的方式执行。在大多数情况下,对于急切的参数求证是可取的,但有时候我们不想让被调用方法阻塞调用者的线程。HopToThreadPoolAwaitable
就能让异步方法剩下的部分派送给线程池中的其他线程来执行,而不是调用者的线程。
Task-like 类型
C#编译器很早就支持了自定义 awaiter
(从C# 5开始)。这项扩展方式确实有用,但也有限制,所有返回值必须为 void
、Task
或 Task<T>
。从 C# 7.2开始,编译器支持 task-like 类型。
是由 AsyncMethodBuilderAttribute
特性标识的类或结构体,且该特性与一个 builder 类型
关联。 为了让 task-like
类型发挥作用,它需要像上一节提到的一样可被等待。基本上,在第一节的方法得到官方支持后,task-like
类型就能够让前面两节的扩展点结合起来。
注:现在你可以自定义上文提到的 AsyncMethodBuilderAttribute
。你可以在我(作者)的 github 仓库
找到例子。
下面有一个将 task-like
定义为结构体的简单例子:
public sealed class TaskLikeMethodBuilder
{
public TaskLikeMethodBuilder()
=> Console.WriteLine(".ctor");
public static TaskLikeMethodBuilder Create()
=> new TaskLikeMethodBuilder();
public void SetResult() => Console.WriteLine("SetResult");
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{
Console.WriteLine("Start");
stateMachine.MoveNext();
}
public TaskLike Task => default(TaskLike);
// AwaitOnCompleted, AwaitUnsafeOnCompleted, SetException
// and SetStateMachine are empty
}
[System.Runtime.CompilerServices.AsyncMethodBuilder(typeof(TaskLikeMethodBuilder))]
public struct TaskLike
{
public TaskLikeAwaiter GetAwaiter() => default(TaskLikeAwaiter);
}
public struct TaskLikeAwaiter : INotifyCompletion
{
public void GetResult() { }
public bool IsCompleted => true;
public void OnCompleted(Action continuation) { }
}
TaskLike
的方法,甚至在方法内我们可以使用不同的 task-like
public async TaskLike FooAsync()
{
await Task.Yield();
await default(TaskLike);
}
task-like
最主要的原因是它能够减少使用异步方法的开销。每一次使用返回 Task<T>
的异步方法,都至少会向堆申请一次内存——因为 task
对象的存在。这对于大部分应用程序来讲还将就,尤其是粗粒度地使用异步方法时。但对于每秒可能跨越数千个小任务的底层代码来说,那就有亿点不优雅了。在这些场景下,减少每次调用的一次堆内存申请可以合理地提高性能。
总结
-
C# 编译器提供了多种方法来让我们扩展异步方法。
-
你可以自定义自己的
AsyncTaskMethodBuilder
类来改变Task-base
异步方法的行为表现。 -
你可以通过实现自定义
awaiter
来让某个对象可被等待。 -
从 C# 7 开始,你可以自定义自己的
task-like
类。
课外阅读
下次,我们会讨论影响异步方法性能的知识点,并且介绍官方最新的 task-like
类型——System.ValueTask
。