异步生锈是强大的,但是难以学习的声誉。有关如何解决最棘手的方面的各种想法,尽管焦点焦点在Tokio 1.0上,我已经能够致力于这些主题的很多焦点。然而,Niko的异步努力再次开始讨论,所以我以为我会花一些时间toparticipate。
在这篇文章中,我收集了一些先前提出的想法并提供了一些新那,将他们绑在一起,探索可能是什么。这种探索不是墨迹,但思想实验:如果我们不必担心现状,我们可以做些什么。对生锈进行重大变化将是破坏性的。我们需要一种严格的方式来确定利弊,以确定流失是值得的。我也敦促您使用OpenMind接近文章。我期待一些方面会产生立即不良反应。尝试将其妥善处理并用开放的思维方式接近它。
在探索不同的路径之前,异步生锈可能需要,我们应该先使用异步编程样式。毕竟,异步编程比使用线程更具挑战性。所以有什么好处?有些人可能会说原因是性能,并且异步代码是迅速的,因为线程太重了。现实更细致。根据详细信息,使用基于线程的Fori / O应用程序可以更快。例如,当存在少于约100个并发连接时,AthReaded Echo Server比AnaSynchronous版本更快。在此时,线程版本开始丢弃,但不会急剧上。
我相信异步的更好原因是它能够有效地建模复杂流控制。例如,暂停或取消Anin-Flight操作的模式是在没有异步编程的情况下具有挑战性。用线程,带有线程,连接之间的协调,并开始添加争用。使用异步编程,我们避免通过在同一线程上的多重连接上操作同步。
Rust的异步模型是使我们能够建模复杂流控制的现象工作。例如,迷你redis的订阅者非常简洁而优雅。然而,这不是所有的阳光和rainbows。入门后,异步生锈的用户报告困惑的曲线。虽然入门感觉很直接,但很容易在意想不到的锋利边缘上倾斜。 Niko和其他人一直在做一个梦幻般的剧目,作为Async Visioneffort的一部分。虽然有几个前沿的人诊所,但我相信异步生锈的最大问题是它可以违反最小的原则。
让我们从一个例子开始。 Alanis学习锈,已阅读生锈书和Tokio指南,并希望Writea玩具聊天服务器。他选择了一个简单的基于线路的协议,并用长度前缀编码每个线。他的线路解析功能如下所示:
异步FN Parse_line(插座:& tcpstream) - >结果<字符串,错误> {让Len =套接字。 read_u32()。等待? ;让mut线= vec! [0; len];插座 。 read_exact(& mut线)。等待? ;让Line = str :: from_utf8(行)? ;好的(线)}
除了异步和等待关键字之外,此代码看起来非常像阻止生锈代码。即使艾伦以前从未写过过生锈,他也可以阅读这个功能并了解它的行为方式,否则他的想法。当测试本文时,他的聊天服务器似乎工作,所以他发送了一个链接到Barbara.unfortonation),在聊天后,服务器崩溃了“Invalidutf-8”错误。艾伦陷入困惑;他检查了他的代码并发现没有明显的错误。
那有什么问题?事实证明,任务使用选择!更高的Inthe Call Stack:
循环{选择! {line_in = parse_line(& socket)=> {如果让某些(line_in)= line_in {bascastring_line(line_in); } else {//连接关闭,退出循环中断; line_out = channel。 recv()=> {write_line(& socket,line_out)。等待; }}}
假设在频道上收到消息,同时Parse_Line正在等待Formore数据,选择!声明中止了PARSE_LINE操作,因此丢失了LOSEIN-进度解析状态。在以下循环迭代中,再次调用PARSE_LINE并从帧的中间开始,从而导致Gibberish。
其中有问题:任何异步生锈功能可能会在呼叫者取消它的任何一个时都可以停止运行,并且与阻塞生锈不同,取消是非典型异步操作。更糟糕的是,没有能力指导新用户努力这个行为。
如果我们可以解决这个问题,以便异步生锈会符合每一步的学习者屏所?如果行为必须偏离期望,那必须是指向学习者在正确的方向上指向学习者的承受。提出,我们希望尽量减少学习过程中的惊喜,特别是早期。
让我们首先来解决意外的取消问题,通过说始终运行到完成(首次提出)。使用完成保证的文件,Alan学习异步生锈表现类似于阻止生锈,但添加了异步和等待关键字。产卵任务AddScurcurency,以及任务之间的频道坐标。而不是选择!收入异步语句,它适用于频道和相同的频道类型(例如,JoinHandle)。
async fn handle_connection(套接字:tcpstream,频道:频道){让reader = arc :: new(套接字);让作家=读者。克隆();让read_task = task :: spawn(异步移动{虽然让一些(line_in)在parse_line(& reader)中。等待{bascess_line(line_in);}});循环{//`clion`和joinhandle都是"频道"类型。选择 ! {res = read_task。加入()=> {//连接关闭,退出循环中断; line_out =通道。 recv()=> {write_line(& writer,line_out)。等待; }}}}}
代码类似于前面的示例,但是因为所有的异步分类都完成并选择!只接受类似频道的类型,调用parse_line()移动到生成的任务。此更改可防止具有当今异步生锈的错误alanencounter。应该试着在选择中调用parse_line()!声明,他将获得一个编译器错误,arecenceation才能产生任务。选择的频道等要求可确保中止丢失分支是安全的。通道可以存储值,并接收值是原子的。失败选择分支不会丢失取消数据。
如果撰写遇到错误,会发生什么?今天,Read_Task一直运行.Instead,Alan希望出错导致优雅地关闭连接和所有任务。不幸的是,这就是我们开始营运到设计挑战的地方。当我们随时丢弃任何异步声明时,取消是微不足道的:降低未来。我们需要一种方法来取消飞行中的运行,因为这种能力是使用异步编程的主要原因。为此,JoinHandle提供了取消()方法:
async fn handle_connection(套接字:tcpstream,频道:频道){让reader = arc :: new(套接字);让作家=读者。克隆();让read_task = task :: spawn(异步移动{同时让一些(line_in)在parse_line(& reader)中。等待?{bascast_line(line_in)?;}确定(())});循环{//`clion`和joinhandle都是"频道"类型。选择 ! {_ = read_task。加入()=> {//连接关闭或我们遇到错误,//退出循环中断; line_out =通道。 recv()=> {如果write_line(& writer,line_out)。等待。 is_err(){read_task。取消 (); read_task。加入 (); }}}}}}
但取消()做什么?它不能立即中止任务,因为现在保证了异步才能完成。我们确实希望消除的任务尽快停止处理和返回。相反,所有资源类型都是已取消的任务将停止运行并返回“中断”错误。使用它们的备份也将导致错误。除了kotlin之外,这种策略类似于Kotlin,jotlin提出了一个例外。如果在取消时read_task在parse_line()中等待socket.read_u32(),则read_u32()函数使用err(io :: errkdrind ::中断)返回默认。这 ?操作员泡起来的任务并导致任务终止。
乍一看,这种行为也可能看起来像任务任意停止,但这不是这种情况。对于Alan,当前的异步生锈Abortbehavior出现,因为任务无限期地悬挂。通过迫使资源,此类购物,返回取消错误,Alan可以遵循CleanationFlow。 Alan可以选择添加Println!语句或使用其他调试程序进行更好地了解已取消的任务如何终止。
Ubbeknst到Alan,他的聊天服务器正在避免使用大多数Syscalls。异步生锈可以透明地利用IO-URING API Chinkensto完成保证的期货和异步。当Alan在Handle_Connection()末尾的TCPStream值下降时,套接字是同步关闭的。为此,TCPStream包括以下异步实现。
NIKO已经拥有使用ASYNC FN内部的PLAusibleProposal。剩下的开放问题是如何处理隐式.awaitPoint。目前,异步等待未来需要.await呼叫。当Avalue超出异步上下文中的范围时,Asyncdrop特性将添加编译器添加的隐藏产量点。这种行为将违反令人遗憾的是最不令人惊讶的。如果有互相明确的话,为什么会有隐含的等待点?
解决“有时隐式丢弃”问题的一个提议需要一个显式功能调用来异步执行丢弃。
当然,如果用户忘记异步丢弃呼叫会发生什么?毕竟,Thecompiler处理掉落带阻塞生锈,这是一个强大的功能。还清,注意上面的片段有一个微妙的错误:the?操作员在读取错误上跳过ASYNC_DROP呼叫。 Rust编译器可以提供警告当Acatch这个问题,但是修复了什么?有没有办法制作?兼容一个显式的异步_drop?
如果,而不是要求显式调用异步丢弃,我们删除了await关键字?在调用Anasync函数之后,ALAN将不再需要使用.Await(例如,Socket.read_u32()。等待)。调用异步上下文中的异步函数时,.await成为隐含。
这看起来可能像今天的生锈一样大,而且它是。但是,我们要质疑我们的假设是好的。在亚伦的“推理特写”博客中布局的标准,它很有意思。隐式的.Await通过唯一在异步语句内闭塞的适用性和上下文依赖性。艾伦只能看看函数虚构,以知道他在异步上下文中。此外,它将容易突出显示屈服点。
删除.await有另一种好处:它带来了异步生锈,符合伯爵锈。阻止的概念已经隐含。通过阻止锈,我们不写my_socket.read(缓冲区).block ?.当他写入Asynchronouschat服务器时,对ALAN的唯一明显的差异成为有必要使用ASYNC关键字函数的函数。艾伦可以在执行TheAnc代码的情况下。 “懒惰期货”的问题消失了,艾伦不能偶然下行并想知道为什么“两个”打印。
async fn my_fn_one(){println! ("一个"); } async fn my_fn_two(){println! ("两个"); } async fn mixup(){let一个= my_fn_one();让两个= my_fn_two();加入 ! (二,一); }
“.await”RFC确实包括一些关于隐性等待的讨论。此时,对隐式等待的最引人注目的争论是,可以中止ASYNC语句的等待呼叫annotated积分。这参数使用完成保证的期货申请更少。当然,对于abort-safeAsync语句,应该保留await关键字吗?这个问题需要回答。无论如何,删除“.await”将是一个很大的变化,需要小心翼翼地接近。可用性研究需要证明该专业人员占缺点。
到目前为止,Alan可以使用异步生锈构建他的聊天服务器,没有学习的新概念或命中意外行为。他了解了选择!但是编译器强制选择在类似频道的类型上。除此之外,Alanadded Async对他的职能,它刚刚工作。他将代码显示为Barbaraand要求将套接字带有弧形。芭芭拉建议Helook进入范围任务作为一种避免分配的方法。
范围任务是CrossBeam的ScopedThreads的异步等效项:Taskthat可以借用Spawner拥有的数据。艾伦可以使用范围任务来避免在他的连接处理程序中的弧。
异步fn handle_connection(套接字:tcpstream,channel:channel){task :: scope(异步| scope | {ver read_task = scope。spawn(async || {artew_in)parse_line(& socket)?{bascestr_line (line_in)?;}确定(())});循环{//`clion`和joinhandle都是"频道"类型。选择!{_ = read_task。join()=> {//连接关闭或我们遇到一个错误,//退出循环破;}。LINE_OUT =信道的recv()=> {如果write_line(安培;写入器,LINE_OUT)。is_err(){断裂;}}} }}); }
确保安全的关键是保证范围在范围内所有任务所有的任务,或者换句话说,确保异步统计数据库。有一个缺点。支持范围的任务需要使“未来::民意调查”不安全,因为轮询完成将来成为必需的素质安全(在Matthias'RFC中覆盖)。加入不安全的不会用手实施未来更困难。作为缓解,我们需要消除几乎所有用户的需求,以便手动实施未来,包括在实现asyncread和asynciterator等特征时。 Ibelieve这是一个可实现的目标。
除了范围任务之外,确保Async语句完成可以在使用IO-Uraing或IntegratingWith C ++期货时从任务值到内核的传递点。在一些情况下,也可以避免产卵子任务的分配,这可能有助于嵌入的上下文,尽管它需要略有不同的API。
使用当今的异步生锈,应用程序可以通过使用SELECT来加入重新生物任务来添加并发性!或futuresundered。到目前为止,我们已经讨论了,选择了!我建议删除FuturiteUnOrdered,因为它是一个令人难题的错误。使用FututesUnOrdered时,它很容易产生任务,期待他们将在后台运行,然后在不发出任何进展时感到惊讶(请参阅状态Quostory)。
让问候="你好" taxo_string(); task :: scope(async | scope | {let mut task_set = scope.task_set();对于i在0..10 {task_set.spawn(async { Println!(" {}从任务{}}}}}}}}}}}}}}}}}}}}});}在task_set中的resync for resync {println!("任务完成{:?}&#34 ;,res);}});
每个生成的任务同时运行,来自产卵任务的借用数据,而taskset实用程序提供类似的API作为未经危险的FuturiteUnOrdered。其他基元,例如缓冲流,也可以实现方向op的范围任务。
有机会探索这些探索这些产品的新并发原语。例如,类似于kotlin的结构化并发性,现在将无法解决。 Matthias先前已经探索了这个空间,但是Butsynchronouse Rust的当前模型使得无法实现。通过SwitchPringAsynchronous Rust保证完成,我们解锁了这整个区域。
在文章的开头,我声称,使用异步程序化,我们有效地模拟复杂的流量控制。我们今天最有效的是我们选择!我还提出在本文中减少选择!仅接受类似频道的基元,强制alan将每次连接的Twotasks产卵同时读写。在产卵时,任务在取消读取操作时会阻止错误,它也可以是可以重写读取操作以容忍意外的取消。 forexample,当在迷你redis中解析帧时,我们首先存储在缓冲区中的接收数据。取消读取操作时,Nodata丢失,因为它位于缓冲区中。下一个读取的呼叫将恢复我们离开的地方。迷你redis读取操作是abort-safe。
如果,而不是限制选择!向频道相同的类型,我们限制它toabort安全操作。从频道接收的是中止 - 安全,但是读取了缓冲的I / O句柄。这里的点是,而不是假设AllaseNous操作是中止 - 安全的,我们要求开发人员选择将#[abort_safe](或异步(中止))替换为其函数定义。本文策略有一些优势。首先,当ALAN正在学习异步时,他不需要知道任何关于中止安全的东西。通过产卵来获得并发的情况,可以在没有概念的情况下实现一切。
#[abort_safe]异步fn Read_line(& mut self) - > IO ::结果<选择<字符串>> {循环{//如果让某些(行)= self,则使用缓冲区中的全行。 parse_line()? {返回OK(线);如果0 == self,则缓冲已缓冲全线的数据被缓冲到了。插座 。 read_buf(& mut self。缓冲区)? {//远程关闭连接。如果是自我。缓冲 。 is_empty(){返回OK(无); }否则{返回错误("通过对等&#34重置连接;进入()); }}}}}
它而不是默认成为安全的语句,而是选择选择。本文策略遵循展开安全的现有模式。当NewDeveloper跳入代码时,注释会通知它们函数秉承中止安全保证。 Rust编译器甚至可以提供使用#[abort_safe]注释的函数的DirectEdal检查和警告。
现在,ALAN可以从带有“选择!”的循环中的read_line()函数。
循环{选择! {line_in =连接。 read_line()? => {如果让某些(line_in)= line_in {bascastring_line(line_in); } else {//连接关闭,退出循环中断; line_out = channel。 recv()=> { 联系 。 write_line(line_out)? ; }}}
#[abort_safe]注释引入了两个异步的两个变体。与非中止安全陈述的混合中止保险箱需要专项。始终可以调用中止安全功能,无论是来自中止 - 安全还是非中止安全的上下文。但是,Rust编译器将防止来自中止安全上下文的非中止安全功能,并提供Ahelpful错误消息。
async fn mescplete(){...}#[abort_safe] async fn can_abort(){//无效呼叫=> compiler错误must_complete(); }
async fn mescplete(){...}#[abort_safe] async fn can_abort(){//有效调用spawn(async {must_complete()})
......