开发进阶:Dotnet Core多路径异步终止
今天用一个简单例子说说异步的多路径终止。我尽可能写得容易理解吧,但今天的内容需要有一定的编程能力。
今天这个话题,来自于最近对gRPC的一些技术研究。
话题本身跟gRPC没有太大关系。应用中,我用到了全双工数据管道这样一个相对复杂的概念。
我们知道,全双工连接是两个节点之间的连接,但不是简单的“请求-响应”连接。任何一个节点都可以在任何时间发送消息。概念上,还是有客户端和服务端的区分,但这仅仅是概念上,只是为了区分谁在监听连接尝试,谁在建立连接。实际上,做一个双工的API比做一个“请求-响应”式的API要复杂得多。
由此,延伸出了另一个想法:做个类库,在库内部构建双工管道,供给消费者时,只暴露简单的内容和熟悉的方式。
为了防止不提供原网址的转载,特在这里加上原文链接:https://www.cnblogs.com/tiger-wang/p/14297970.html
一、开始
假设我们有这样一个API:
- 客户端建立连接
- 有一个
SendAsync
消息从客户端发送到服务器 - 有一个
TryReceiveAsync
消息,试图等待来自服务器的消息(服务器有消息发送为True,返之为False) - 服务器控制数据流终止,如果服务器发送完最后一条消息,则客户端不再发送任何消息。
接口代码可以写成这样:
interface ITransport<TRequest, TResponse> : IAsyncDisposable
{
ValueTask SendAsync(TRequest request, CancellationToken cancellationToken);
ValueTask<(bool Success, TResponse Message)> TryReceiveAsync(CancellationToken cancellationToken);
}
忽略连接的部分,代码看起来并不复杂。
下面,我们创建两个循环,并通过枚举器公开数据:
ITransport<TRequest, TResponse> transport;
public async IAsyncEnumerable<TResponse> ReceiveAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
while (true)
{
var (success, message) =
await transport.TryReceiveAsync(cancellationToken);
if (!success) break;
yield return message;
}
}
public async ValueTask SendAsync(IAsyncEnumerable<TRequest> data, CancellationToken cancellationToken)
{
await foreach (var message in data.WithCancellation(cancellationToken))
{
await transport.SendAsync(message, cancellationToken);
}
}
这里面用到了异步迭代器相关的概念。如果不明白,可以去看我的另一篇专门讨论异步迭代器的文章,【传送门】。
二、解决终止标志
好像做好了,我们用循环接收和发送,并传递了外部的终止标志给这两个方法。
真的做好了吗?
还没有。问题出在终止标志上。我们没有考虑到这两个流是相互依赖的,特别是,我们不希望生产者(使用SendAsync的代码)在任何连接失败的场景中仍然运行。
实际上,会有比我们想像中更多的终止路径:
- 我们可能已经为这两个方法提供了一个外部的终止令牌,并且这个令牌可能已经被触发
ReceiveAsync
的消费者可能已经通过WithCancellation
提供了一个终止令牌给GetAsyncEnumerator
,并且这个令牌可能已经被触发- 我们的发送/接收代码可能出错了
ReceiveAsync
的消费者在数据获取到中途,要终止获取了 - 一个简单的原因是处理收到的数据时出错了SendAsync
中的生产者可能发生了错误
这只是一些可能的例子,但实际的可能会更多。
本质上,这些都表示连接终止,因此我们需要以某种方式包含所有这些场景,进而允许发送和接收路径之间传达问题。换句话说,我们需要自己的CancellationTokenSource
。
显然,这种需求,用库来解决是比较完美的。我们可以把这些复杂的内容放在一个消费者可以访问的单一API中:
public IAsyncEnumerable<TResponse> Duplex(IAsyncEnumerable<TRequest> request, CancellationToken cancellationToken = default);
这个方法:
- 允许它传入一个生产者
- 通话它传入一个外部的终止令牌
- 有一个异步的响应返回
使用时,我们可以这样做:
await foreach (MyResponse item in client.Duplex(ProducerAsync()))
{
// ... todo
}
async IAsyncEnumerable<MyRequest> ProducerAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
for (int i = 0; i < 100; i++)
{
yield return new MyRequest(i);
await Task.Delay(100, cancellationToken);
}
}
上面这段代码中,我们ProducerAsync
还没有实现太多内容,目前只是传递了一个占位符。稍后我们可以枚举它,而枚举行为实际上调用了代码。
回到Duplex
。这个方法,至少需要考虑两种不同的终止方式:
- 通过
cancellationToken
传入的外部令牌 - 使用过程中可能传递给
GetAsyncEnumerator()
的潜在的令牌
这儿,为什么不是之前列出的更多种终止方式呢?这儿要考虑到编译器的组合方式。我们需要的不是一个CancellationToken
,而是一个CancellationTokenSource
。
public IAsyncEnumerable<TResponse> Duplex(IAsyncEnumerable<TRequest> request, CancellationToken cancellationToken = default) => DuplexImpl(transport, request, cancellationToken);
private async static IAsyncEnumerable<TResponse> DuplexImpl(ITransport<TRequest, TResponse> transport, IAsyncEnumerable<TRequest> request, CancellationToken externalToken, [EnumeratorCancellation] CancellationToken enumeratorToken = default)
{
using var allDone = CancellationTokenSource.CreateLinkedTokenSource(externalToken, enumeratorToken);
// ... todo
}
这里,DuplexImpl
方法允许枚举终止,但又与外部终止标记保持分离。这样,在编译器层面不会被合并。在里面,CreateLinkedTokenSource
反倒像编译器的处理。
现在,我们有一个CancellationTokenSource
,需要时,我们可能通过它来终止循环的运行。
using var allDone = CancellationTokenSource.CreateLinkedTokenSource(externalToken, enumeratorToken);
try
{
// ... todo
}
finally
{
allDone.Cancel();
}
通过这种方式,我们可以处理这样的场景:消费者没有获取所有数据,而我们想要触发allDone
,但是我们退出了DuplexImpl
。这时候,迭代器的作用就很大了,它让程序变得更简单,因为用了using
,最终里面的任何内容都会定位到Dispose/DisposeAsync
。
下一个是生产者,也就是SendAsync
。它也是双工的,对传入的消息没有影响,所以可以用Task.Run
作为一个独立的代码路径开始运行,而如果生产者出现错误,则终止发送。上边的todo部分,可以加入:
var send = Task.Run(async () =>
{
try
{
await foreach (var message in request.WithCancellation(allDone.Token))
{
await transport.SendAsync(message, allDone.Token);
}
}
catch
{
allDone.Cancel();
throw;
}
}, allDone.Token);
// ... todo: receive
await send;
这里启动了一个生产者的并行操作SendAsync
。注意,这里我们用标记allDone.Token
把组合的终止标记传递给生产者。延迟await
是为了允许ProducerAsync
方法里可以使用终止令牌,以满足复合双工操作的生命周期要求。
这样,接收代码就变成了:
while (true)
{
var (success, message) = await transport.TryReceiveAsync(allDone.Token);
if (!success) break;
yield return message;
}
allDone.Cancel();
最后,把这部分代码合在一起看看:
private async static IAsyncEnumerable<TResponse> DuplexImpl(ITransport<TRequest, TResponse> transport, IAsyncEnumerable<TRequest> request, CancellationToken externalToken, [EnumeratorCancellation] CancellationToken enumeratorToken = default)
{
using var allDone = CancellationTokenSource.CreateLinkedTokenSource(externalToken, enumeratorToken);
try
{
var send = Task.Run(async () =>
{
try
{
await foreach (var message in request.WithCancellation(allDone.Token))
{
await transport.SendAsync(message, allDone.Token);
}
}
catch
{
allDone.Cancel();
throw;
}
}, allDone.Token);
while (true)
{
var (success, message) = await transport.TryReceiveAsync(allDone.Token);
if (!success) break;
yield return message;
}
allDone.Cancel();
await send;
}
finally
{
allDone.Cancel();
}
}
三、总结
相关的处理就这么多。这里实现的关键点是:
- 外部令牌和枚举器令牌都对
allDone
有贡献 - 传输中发送和接收代码使用
allDone.Token
- 生产者枚举使用
allDone.Token
- 任何情况下退出枚举器,
allDone
都会被终止- 如果传输接收错误,则
allDone
被终止 - 如果消费者提前终止,则
allDone
被终止
- 如果传输接收错误,则
- 当我们收到来自服务器的最后一条消息后,
allDone
被终止 - 如果生产者或传输发送错误,
allDone
被终止
最后多说一点,关于ConfigureAwait(false)
:
默认情况下,await
包含一个对SynchronizationContext.Current
的检查。除了表示额外的上下文切换之外,在UI应用程序的情况下,它也意味着在UI线程上运行不需要在UI线程上运行的代码。库代码通常不需要这样做。因此,在库代码中,通常应该在所有用到await的地方使用. configureawait (false)
来绕过这个检查。而在一般应用程序的代码中,应该默认只使用await
而不使用ConfigureAwait
,除非你知道你在做什么。
![]() |
微信公众号:老王Plus 扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送 本文版权归作者所有,转载请保留此声明和原文链接 |
开发进阶:Dotnet Core多路径异步终止的更多相关文章
- ubuntu上部署windows开发的dotnet core程序
目标:完成windows上开发的dotnet core程序部署至linux服务器上(Ubuntu 14.04) windows上开发dotnet core很简单,安装好VS2017,建立相关类型的项目 ...
- dotnet core 项目
项目 常用命令 我们使用dotnet core 命令行来创建项目及进行编译,发布等,比较常用的dotnet core 命令 如下: dotnet new [arguments] [options] 创 ...
- dotnet core 开发体验之Routing
开始 回顾上一篇文章:dotnet core开发体验之开始MVC 里面体验了一把mvc,然后我们知道了aspnet mvc是靠Routing来驱动起来的,所以感觉需要研究一下Routing是什么鬼. ...
- dotnet core多平台开发体验
前言 随着net core rc2的发布,园子里面关于net core的入门文章也也多了起来,但是大多数都是在一个平台上面来写几个简单的例子,或者是在解释代码本身,并没有体现说在一个平台上面创建一个项 ...
- dotnet core多平台开发体验(mac os x 、windows、linux)
前言 随着net core rc2的发布,园子里面关于net core的入门文章也也多了起来,但是大多数都是在一个平台上面来写几个简单的例子,或者是在解释代码本身,并没有体现说在一个平台上面创建一个项 ...
- dotNet Core开发环境搭建及简要说明
一.安装 .NET Core SDK 在 Windows 上使用 .NET Core 的最佳途径:使用Visual Studio. 免费下载地址: Visual Studio Community 20 ...
- ASP.NET Core 中文文档 第二章 指南(8) 使用 dotnet watch 开发 ASP.NET Core 应用程序
原文:Developing ASP.NET Core applications using dotnet watch 作者:Victor Hurdugaci 翻译:谢炀(Kiler) 校对:刘怡(Al ...
- DotNet Core 1.0 集成 CentOS 开发与运行环境部署
一. DotNet Core 1.0 开发环境部署 操作系统安装 我们使用CentOS 7.2.1511版本. 安装libunwind库 执行:sudo yum install libunwi ...
- dotnet core开发体验之开始MVC
开始 在上一篇文章:dotnet core多平台开发体验 ,体验了一把dotnet core 之后,现在想对之前做的例子进行改造,想看看加上mvc框架是一种什么样的体验,于是我就要开始诞生今天的这篇文 ...
随机推荐
- Docker(三):Docker安装MySQL
查找MySQL镜像 镜像仓库 https://hub.docker.com/ 下拉镜像 docker pull mysql:5.7 查看镜像 docker images 创建MySQL容器 命令式启动 ...
- redis位操作
setbit 设置指定key的偏移量处的值 key:键值 offset:二进制数据的偏移量(注意从左开始为第0位) value:要设置的值(0或1) getbit 获取对应key的offset处的值 ...
- 个人微信公众号搭建Python实现 -个人公众号搭建-运行run方法的编写(14.3.3)
@ 目录 1.主要逻辑 2.代码 关于作者 1.主要逻辑 使用的是flask服务器 就使用一个函数处理请求 第一个是验证服务器,返回微信服务器给的字符串就表示验证成功 第二是要处理微信服务器发送过来的 ...
- BullseyeCoverage——C/C++代码覆盖率分析工具
安装 官网下载地址: https://www.bullseye.com/cgi-bin/download 安装过程中需要License, 可以申请评估版本的key或者直接从官网渠道购买. Ubuntu ...
- 神经网络高维互信息计算Python实现(MINE)
论文 Belghazi, Mohamed Ishmael, et al. " Mutual information neural estimation ." Internatio ...
- Python进行拉勾网数据爬取框架与思路
爬取内容 用交互式的方式输入想查询的职位并循环输出职位简要信息,为了方便阅读,本文使用到的变量并不是以目标变量的英文来命名,而是为了方便而取的变量名,各位大牛请谅解.(因贵网站爬取一定量数据后需要登陆 ...
- 记一次Ajax请求MVC调优
问题描述 ASP.NETMVC项目,编辑页面赋值操作,ajax请求很慢. 对同一个控制器有2个请求 一个为获取该表的树形结构数据(绑定下拉框) 一个为获取某实体的单条记录(编辑操作) 排查过程 数据库 ...
- Code-Review-Maven编译(第三方jar包引用)
Code-Review-SpringBoot-Maven编译(第三方jar包引用) 在使用maven编译项目时,有时候咱们可能会使用一些第三方的jar包依赖库,比如第三方支付类的接入,大多出于安全考虑 ...
- eclips快捷键
所谓"工欲善其事必先利其器",程序写多了,对于快捷键总有些特别的偏爱.在众多编辑器中,Eclipse算是用的比较多,也是最熟的. 最常用(也是最爱的:)) Ctrl+' : 自动 ...
- stm32之can总线过滤器研究
stm32的can总线的配置如下: CAN_InitStructure.CAN_TTCM=DISABLE;//禁止时间触发通信模式 CAN_InitStructure.CAN_A ...