[翻译]扩展C#中的异步方法
翻译自一篇博文,原文:Extending the async methods in C#
异步系列
- 剖析C#中的异步方法
- 扩展C#中的异步方法
- C#中异步方法的性能特点。
- 用一个用户场景来掌握它们
在上一篇中我们讨论了C#编译器是如何转换异步方法的。在这一篇,我们将重点讨论C#编译器为自定义异步方法的行为提供的可扩展性。
关于如何控制异步方法机制有3种方法:
- 在
System.Runtime.CompilerServices命名空间中提供你自己的async method builder。 - 使用自定义的task awaiter。
- 定义你自己的“类任务”(task-like)类型
System.Runtime.CompilerServices 命名空间中的自定义类型
上一篇文章中我们已经知道,异步方法被C#编译器转换从而生成的状态机是依靠于某些预定义的类型的。但是C#编译器却并不一定要求这些众所周知的类型来自于某个特定的程序集。例如,你可以在你的项目中提供自己对AsyncVoidMethodBuilder的实现,然后C#编译器就会把异步机制“绑定”到你的自定义类型。
这种方式可以很好地让我们来探索底层的转换,以及运行时发生了什么:
namespace System.Runtime.CompilerServices
{
// 你自己项目中的AsyncVoidMethodBuilder.cs
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
// 和SetStateMachine都不提供具体实现
}
}
现在,在你的项目中的每一个异步方法都会使用这个自定义的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方法来测试带有await字句,也就是返回一个未完成的任务的异步方法的行为。完整的例子可以在这里找到:github.
若想改变async Task方法和async Task<T>方法的行为,则需要提供自定义的AsyncTaskMethodBuilder和AsyncTaskMethodBuilder的实现。
这些类型的完整示例可以在我的名为EduAsync的github项目中的AsyncTaskBuilder.cs和AsyncTaskMethodBuilderOfT.cs中分别找到。
感谢Jon Skeet为这个项目带来的灵感。这真的是一个很好的方法来更加深入地学习异步机制。
自定义的awaiter
前面的例子并不优雅,显然不适合用于生产环境。我们可以通过这种方式学习异步机制,但肯定不希望在自己的代码库中看到这样的代码。C#的作者们已在编译器中内置了适当的可扩展点,从而允许在异步方法中“等待(await)”不同的类型。
为了使一个类型是“可等待的”(即在await表达式的上下文中是有效的),这个类型应该遵循一个特殊的模式:
- 编译器应该能找到一个叫
GetAwaiter实例方法或扩展方法。这个方法的返回类型应该满足某些条件: - 实现了
INotifyCompletion接口。 - 有
bool IsCompleted {get;}属性和T 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);
}
这个例子可能看起来过于人为,但这个可扩展性其实非常有用,并且会被运用在实际中。比如,Reactive Extensions for .NET就提供了一个自定义的awaiter用于在异步方法中等待(await)IObservable<T>的实例。BCL自己也有用于Task.Yield和HopToThreadPoolAwaitable的YieldAwaitable:
public struct HopToThreadPoolAwaitable : INotifyCompletion
{
public HopToThreadPoolAwaitable GetAwaiter() => this;
public bool IsCompleted => false;
public void OnCompleted(Action continuation) => Task.Run(continuation);
public void GetResult() { }
}
下面的单元测试展示了上面的awaiter的运用:
[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语句之前的部分)是同步执行的。在大多数情况下,这是没问题并且对“预先参数验证(eager argument validation )”是必要的,但有时候我们希望确保方法的主体不会阻塞调用者的线程。HopToThreadPoolAwaitable确保了方法的剩余部分在一个线程池线程而不是调用者线程中执行。
“类任务”(Task-like)类型
从支持async/await的编译器的第一个版本(即C# 5)开始,就可以自定义awaiter了。这个可扩展性十分有用但是却是有限的,因为所有的异步方法都必须返回void, Task或Task<T>。从C# 7.2开始,编译器支持“类任务”类型。
“类任务”类型是一个class或者struct,它与一个通过AsyncMethodBuilderAttribute标识的builder类型 相关联。要使“类任务”类型有用,它应该像我们前面描述的awaiter那样是可等待的。基本上,“类任务”类型结合了前面描述的两种可扩展性的方法,并且使第一种方法得到了正式支持。
现在你还必须自己定义这个attribute,例子:my github repo。
下面是一个简单的例子,一个定义为struct的自定义“类任务”类型:
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类型的方法了,甚至可以在方法内部使用不同的“类任务”类型:
public async TaskLike FooAsync()
{
await Task.Yield();
await default(TaskLike);
}
使用“类任务”类型的主要原因是为了减少异步操作的开销。每一个返回Task<T>的异步操作都至少在托管堆中分配了一个对象——这个任务本身。这对大多数应用程序来说都不是什么问题,特别是当他们处理粗粒度的异步操作时。但对可能会造成每秒执行数千个小任务的基础结构级代码来说,情况并非如此。对于这样的场景,每次调用减少一次分配可以合理地提高性能。
异步模式可扩展性
- C#编译器提供了扩展异步方法的各种方式。
- 你可以通过提供自己的
AsyncTaskMethodBuilder类型来改变现有的基于Task的异步方法的行为。 - 你可以通过实现“可等待模式(awaitable pattern)”使一个类型变得“可等待(awaitable)”。
- 从C# 7开始你可以构建自己的“类任务”(task-like)类型。
其他参考资料
下次我们将讨论异步方法的性能特点,我们将会看到最新的“类任务”(task-like)类型System.ValueTask是如何影响性能的。
[翻译]扩展C#中的异步方法的更多相关文章
- [翻译]剖析C#中的异步方法
翻译自一篇博文,原文:Dissecting the async methods in C# 有些括号里的是译注或我自己的理解. 异步系列 剖析C#中的异步方法 扩展C#中的异步方法 C#中异步方法的性 ...
- 又踩.NET Core的坑:在同步方法中调用异步方法Wait时发生死锁(deadlock)
之前在将 Memcached 客户端 EnyimMemcached 迁移 .NET Core 时被这个“坑”坑的刻骨铭心(详见以下链接),当时以为只是在构造函数中调用异步方法(注:这里的异步方法都是指 ...
- 【AspNetCore】【WebApi】扩展Webapi中的RouteConstraint中,让DateTime类型,支持时间格式化(DateTimeFormat)
扩展Webapi中的RouteConstraint中,让DateTime类型,支持时间格式化(DateTimeFormat) 一.背景 大家在使用WebApi时,会用到DateTime为参数,类似于这 ...
- Expression Tree 扩展MVC中的 HtmlHelper 和 UrlHelper
表达式树是LINQ To everything 的基础,同时各种类库的Fluent API也 大量使用了Expression Tree.还记得我在不懂expression tree时,各种眼花缭乱的A ...
- ios怎样实现快速将显卡中数据读出压缩成视频在cocos2dx扩展开发中
如果解决ios怎样实现快速将显卡中数据读出压缩成视频在cocos2dx扩展开发中 手机平台性能是个关键问题. 压缩视频分成3个步骤: 读取显卡数据, 使用编码器压缩,保存文件. 使用libav 压缩的 ...
- 将基于Nullable<T>的类型转换实现在扩展方法中
三.将基于Nullable<T>的类型转换实现在扩展方法中 从上面的介绍我们可以得出这样的结论:如果类型T1和T2能够相互兼容,我们可以借助Convert将T1类型对象转换成T2类型,然后 ...
- chrome扩展程序中以编程方式插入内容脚本不生效的问题
chrome扩展程序中内容脚本有两种插入方式:(https://crxdoc-zh.appspot.com/extensions/content_scripts) 1. 清单文件: 这种方式会在打开每 ...
- 「翻译」Unity中的AssetBundle详解(二)
为AssetBundles准备资源 使用AssetBundles时,您可以随意将任何Asset分配给所需的任何Bundle.但是,在设置Bundles时,需要考虑一些策略.这些分组策略可以使用到任何你 ...
- 「翻译」Unity中的AssetBundle详解(一)
AssetBundles AssetBundle是一个存档文件,其中包含平台在运行时加载的特定资产(模型,纹理,预制,音频剪辑,甚至整个场景).AssetBundles可以表示彼此之间的依赖关系;例如 ...
随机推荐
- MySQL安装过程中遇到的错误代码为1045的解决方法
mysql的安装包,及其图形化破解软件:https://pan.baidu.com/s/1PIzaEGpC9QEPUwZ8OowhCw 二级压缩包下边的 视图化管理软件:Navicat.exe 发 ...
- Django框架——基础之视图系统(View.py)
Django框架之View.py(视图文件) 1. 视图简介 视图层是Django处理请求的核心代码层,我们大多数Python代码都集中在这一层面. 它对外接收用户请求,对内调度模型层和模版层,统合数 ...
- SSD源码解读——损失函数的构建
之前,对SSD的论文进行了解读,可以回顾之前的博客:https://www.cnblogs.com/dengshunge/p/11665929.html. 为了加深对SSD的理解,因此对SSD的源码进 ...
- 一文全面了解NB-IoT技术优势及特点
1.NB-IOT多输入多输出技术 NB-IoT可以利用多天线技术抑制信道传输衰弱,获得分集增益.空间复用增益和阵列增益,在发送端和接收端均采用多天线实现信号同时发送和接收: 因此就形成了一个并行的多空 ...
- Jmeter 常见逻辑控制器详解
简介 Jmeter有很多逻辑控制器,可以控制请求的执行顺序和执行逻辑,本文就Jmeter常见的逻辑控制器做一个详细的描述,并通过示例让大家了解逻辑控制器的作用. 代码的逻辑分支通常有: 条件判断I ...
- 程序中的一些限制(基于Linux系统C语言)
今天突然想起来几个问题,在程序运行起来时,存在一些限制: 1,数组的长度(成员的个数)存在限制!(数组定义的空间大小)2,一个进程里打开的文件数.3,一个文件的名字的长度.4,一个进程里创建线程的个数 ...
- jmeter解析response里的json对象和数组
1.解析提取json对象 2.解析提取json数组 注意,标红这里是从0开始计数 提取最后一个数组
- OSM全球地图MBTiles,非postgresql方式。
介绍: https://www.cnblogs.com/i-gps/p/3919475.html 下载和使用: https://openmaptiles.org/ OSM pbf转换: https:/ ...
- MyEclipse使用教程:导航代码(一)
[MyEclipse CI 2019.4.0安装包下载] 无论是在文件之间导航还是在文件中导航,都可以使用大量导航工具来加快工作流程.目前这些导航工具可在MyEclipse,CodeMix中使用. 快 ...
- sublime text 前端插件安装
Package Control安装 打开sublime编辑器,ctrl + ` 打开安装PackageControl界面: sublime text3: import urllib.request,o ...