本文翻译自

https://blogs.msdn.microsoft.com/seteplia/2018/01/11/extending-the-async-methods-in-c/ 在前一篇博客文章中,我们讨论了c#编译器如何转换异步方法。在这篇文章中,我们将关注c#编译器为定制异步方法的行为提供的扩展点。 有三种方法可以控制异步方法的机制: 1.在System.Runtime.CompilerServices中提供您自己的async方法构建器。 2.使用自定义task awaiter。 3.定义自己的任务类型。

自定义在System.Runtime.CompilerServices命名空间的类型[toc]

从上一篇文章中我们知道,c#编译器将异步方法转换成一个生成的状态机,它依赖于一些预定义的类型。但是c#编译器并不期望这些已知的类型来自特定的程序集。例如,您可以在项目中提供自己的AsyncVoidMethodBuilder实现,c#编译器将“绑定”异步机制到您的自定义类型。 这是一个很好的方法来探索底层的转换是什么,看看在运行时发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
}
}

现在,项目中的每个async方法都将使用AsyncVoidMethodBuilder的自定义版本。我们可以测试这与一个简单的异步方法:

1
2
3
4
5
6
7
8
9
[Test]
public void RunAsyncVoid()
{
Console.WriteLine("Before VoidAsync");
VoidAsync();
Console.WriteLine("After VoidAsync");

async void VoidAsync() { }
}

Test输出如下 Before VoidAsync .ctor Start SetResult After VoidAsync 您可以实现UnsafeAwaitOnComplete方法来测试异步方法的行为,并使用await子句返回未完成的任务。完整的例子可以在github上找到。 要更改async Task和_async Task_方法的行为,您应该提供自己版本的AsyncTaskMethodBuilder和AsyncTaskMethodBuilder 关于这些类型的完整示例可以在我的github项目中找到,该项目名为 EduAsync, 分别位于AsyncTaskBuilder.csAsyncTaskMethodBuilderOfT.cs中。 感谢Jon Skeet对这个项目的启发。这确实是深入学习异步机制的好方法。

自定义awaiters

前面的示例是闹着玩的,不适合正式项目。我们可以通过这种方式学习异步机制,但是您肯定不希望在代码库中看到这样的代码。c#语言的作者在编译器中内置了适当的扩展点,允许在异步方法中“等待”不同的类型。 为了使类型成为“awaitable”(即在await表达式上下文中有效),类型应该遵循一种特殊的模式: 编译器应该能够找到一个名为GetAwaiter的实例或扩展方法。该方法的返回类型应符合一定的要求: 该类型应该实现INotifyCompletion接口。 类型应该具有bool IsCompleted {get;}属性和T GetResult()方法。 这意味着我们可以很容易地使Lazy可选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}
1
2
3
4
5
6
public static async Task Foo()
{
var lazy = new Lazy<int>(() => 42);
var result = await lazy;
Console.WriteLine(result);
}

这个例子可能看起来太做作了,但是这个扩展点实际上非常有用,并且大量应用。例如Reactive Extensions for .NET提供了一个定制的awaiter,用于在异步方法中等待IObservable实例。BCL本身有YieldAwaitable被Task.Yield和HopToThreadPoolAwaitable使用:

1
2
3
4
5
6
7
8
public struct HopToThreadPoolAwaitable : INotifyCompletion
{
public HopToThreadPoolAwaitable GetAwaiter() => this;
public bool IsCompleted => false;

public void OnCompleted(Action continuation) => Task.Run(continuation);
public void GetResult() { }
}

下面的单元测试演示了最后一个正在运行的awaiter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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);
}
}

任何“async”方法的第一部分(在await语句之前)都是同步运行的。在大多数情况下,这对于急切的参数验证是很好的,也是可取的,但有时我们希望确保方法主体不会阻塞调用者的线程。HopToThreadPoolAwaitable确保方法的其余部分在线程池线程中执行,而不是在调用者的线程中执行。

任务类型

这个扩展点非常有用,但是有限,因为所有的异步方法都应该返回void, Task或者Task。从C# 7.2开始,编译器支持任务类型。 Task-like是一个类或结构,它具有AsyncMethodBuilderAttribute(**)标识的关联构建器类型。为了使类似于任务的类型有用,它应该以我们在前一节中描述的方式可用。基本上,task-like类型结合了前面描述的前两个扩展点,实现了官方支持的第一种方式。 今天,您必须自己定义这个属性。示例可以在我的github repo中找到。 下面是一个定义为struct的自定义类任务类型的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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) { }
}

现在我们可以定义一个返回类任务类型的方法,甚至在方法体中使用不同的类任务类型:

1
2
3
4
5
public async TaskLike FooAsync()
{
await Task.Yield();
await default(TaskLike);
}

具有类似任务类型的主要原因是能够减少异步操作的开销。每个返回Task的异步操作都会在托管堆中分配至少一个对象——任务本身。这对于绝大多数应用程序来说都是非常好的,特别是当它们处理粗粒度异步操作时。但是,对于每秒可以跨越数千个小任务的基础结构级代码来说,情况并非如此。对于这种场景,减少每次调用的分配可以合理地提高性能。

异步模式可扩展性

1.c#编译器为扩展异步方法提供了多种方法。 2.您可以通过提供自己版本的AsyncTaskMethodBuilder类型来更改现有基于任务的异步方法的行为。 3.您可以通过实现“awaitable模式”使类型“awaitable”。 4.从c# 7开始,您可以构建自己的类似于任务的类型。

额外的引用

剖析c#中的异步方法 EduAsync repo在github上。

鸽子

下次我们将讨论异步方法的perf特性,并将看到最新的类任务值类型如何调用System.ValueTask会影响性能。