
每种支持并行(竞争性,异步)计算的语言都需要一种并行运行代码的方式。 以下是来自不同API的示例:
go myfunc(); // Golang pthread_create(&thread_id, NULL, &myfunc); /* C with POSIX threads */ spawn(modulename, myfuncname, []) % Erlang threading.Thread(target=myfunc).start() # Python with threads asyncio.create_task(myfunc()) # Python with asyncio
符号和术语有很多选择,但是一种语义是与主程序并行运行myfunc
并继续执行父线程 (例如“控制流”)。
另一种选择是回调 :
QObject::connect(&emitter, SIGNAL(event()), // C++ with Qt &receiver, SLOT(myfunc())) g_signal_connect(emitter, "event", myfunc, NULL) /* C with GObject */ document.getElementById("myid").onclick = myfunc; // Javascript promise.then(myfunc, errorhandler) // Javascript with Promises deferred.addCallback(myfunc) # Python with Twisted future.add_done_callback(myfunc) # Python with asyncio
再次,表示法发生了变化,但是所有示例均如此,以便从当前时刻开始,如果发生某事件,则myfunc
开始。 设置了回调后,控件将返回并且调用函数将继续。 (有时将回调包装在方便的 组合函数或Twisted样式的协议中 ,但基本思想没有改变。)
而且...仅此而已。 采取任何流行的并发通用语言,您可能会发现它属于这些范例之一(有时两者均为asyncio)。
但不是我新的怪异的Trio库。 她没有使用这些方法。 相反,如果我们要并行运行myfunc
和anotherfunc
,我们将编写如下代码:
async with trio.open_nursery() as nursery: nursery.start_soon(myfunc) nursery.start_soon(anotherfunc)
托儿所-托儿所
第一次面对“托儿所”的设计,人们迷路了。 为什么会有上下文管理器 (带块)? 这是什么托儿所,为什么需要执行任务? 然后人们了解到托儿所会干扰其他框架中的常规方法并生气。 一切似乎都离奇,具体且过于高级,以至于不能成为基本的基本知识。 所有这些都是可以理解的反应! 但是要忍受一点。
在本文中,我想说服您,托儿所不是一种时尚,而是一种用于控制执行流程的新原语,就像循环和函数调用一样。 此外,上面讨论的方法(创建线程和注册回调)需要丢弃,并由托儿所代替。
听起来太大胆了吗? 但这已经发生了:一旦goto
广泛用于控制程序的行为。 现在这是一个笑的机会:

几种语言仍然具有所谓的goto
,但是它的功能比原始的goto
更为有限。 而且在大多数语言中根本不是。 他怎么了 这个故事令人惊讶地相关,尽管由于其古老而对大多数人都不熟悉。 让我们提醒自己goto
什么,然后看看这对异步编程有何帮助。
目录
- 什么是去?
- 怎么了
- 发生了什么事?
- 关于“ Go”类型表达式的危险
- 去表达式打破抽象。
- go-expressions破坏了开放资源的自动清理功能。
- go表达式破坏错误处理。
- 没有更多的去
- 苗圃作为围棋的结构替代品
- 托儿所保留功能的抽象。
- 苗圃支持动态添加任务。
- 您仍然可以离开托儿所。
- 您可以识别可作为托儿所的新类型。
- 不,但是,托儿所总是在等待内部所有任务的完成。
- 可以自动清理资源。
- 漏洞修复工程。
- 勇往直前的新世界
- 苗圃实践
- 结论
- 留言
- 致谢
- 脚注
- 关于作者
- 延续性
什么是goto
?
第一台计算机是使用assembler或什至更原始的方式进行编程的。 这不是很方便。 因此,在1950年代,诸如IBM的John Backus和Remington Rand的 Grace Hopper之类的人开始开发诸如FORTRAN和FLOW-MATIC (以其直接后代COBOL闻名)的语言。
当时FLOW-MATIC雄心勃勃。 您可以将它视为Python的伟大祖父-它是主要为人开发的第一门语言,而第二种是为计算机开发的语言。 他看起来像这样:

请注意,与现代语言不同,没有条件的if
块,循环或函数调用-实际上根本没有块或缩进。 这只是一个顺序的表达式列表。 并非因为该程序太短而无法要求控制语句 ( JUMP TO
除外)-只是尚未发明这种语法!

相反,FLOW-MATIC有两个选项可以控制执行流程。 通常,流程是一致的-从顶部开始向下移动,一次表达一次。 但是,如果执行特殊的JUMP TO
表达式,它可能会控制其他位置。 例如,表达式(13)跳转到表达式(2):

就像从本文开头开始的并行操作的原始方法一样,在如何称呼这种“单向跳转”操作上也没有达成共识。 在清单中,这是JUMP TO
,但是goto
历史上已经扎根(例如“ go go”),我在这里使用它。
这是此小程序中完整的goto
跳转集:

这不仅使您感到困惑! FLOW-MATIC直接从汇编程序继承了这种基于跳转的编程风格。 它功能强大,非常接近计算机硬件的实际工作方式,但是直接使用它非常困难。 此箭头是发明“意大利面代码”的原因。
但是为什么goto
会引起这样的问题? 为什么有些控制语句好而另一些则不好? 如何选择好的? 当时这是完全无法理解的,如果您不了解问题,将很难解决。
怎么go
?
让我们偏离我们的故事。 每个人都知道goto
不好,但这与异步有什么关系? 查看Golang中著名的go
表达式,该表达式用于生成新的“ goroutine”(轻量级流):
// Golang go myfunc();
是否可以绘制其执行流程图? 它与上图略有不同,因为此处流是分开的。 让我们这样画:

这里的颜色表示要同时选择两条路径。 从父goroutine(绿线)的角度来看, 控制流是按顺序执行的:它从上方开始,然后立即下降。 同时,从后代函数(淡紫色线)的角度来看,流从上方进入,然后跳入myfunc
的主体。 与常规函数调用不同,它有一个单向跳转-从myfunc
开始,我们切换到一个全新的堆栈 , 运行时立即忘记了我们来自哪里。
显然我的意思是调用堆栈
但这不仅适用于Golang。 此图适用于本文开头列出的所有原语(控件):
- 线程库通常返回某种控制对象,该对象将允许它们稍后加入线程-但这是语言本身一无所知的独立操作。 创建新线程的原语具有上图所示。
- 回调注册在语义上等效于创建后台线程(尽管很明显实现不同),它是:
a)被阻止,直到事件发生,然后
b)启动回调函数
因此,就高级控制运算符而言,回调注册是与go
相同的表达式。 - 对于
Futures
和Promises
同样的事情-当您运行该函数并返回Promise
,这意味着它计划在后台工作并返回一个控件对象以稍后获得结果(如果需要)。 从管理语义的角度来看,它与创建流程相同。 之后,将回调传递给Promis,然后如上一段所述。
这种相同的模式以多种形式显示出来-关键的相似之处在于,在所有这些情况下,控制流都是分开的-跳转到新线程,但是父线程返回到调用它的线程。 知道要看什么,您到处都会看到它! 这是一个有趣的游戏(至少对于某些类型的人而言)!
仍然令我烦恼的是,此类控制语句没有标准名称。 我使用表达式“ go”来称呼它们,就像“ goto”已成为所有goto
表达式的通用术语goto
。 为什么go
? 原因之一是Golang为我们提供了这种语法的非常干净的示例。 另一个是:

注意到相似之处吗? 是的goto
是goto
形式之一。
异步程序因编写和分析的困难而臭名昭著。 以及基于goto
程序。 由goto
引起的问题大多以现代语言解决。 如果我们学习如何修复goto
,这是否有助于创建更方便的异步API? 让我们找出答案!
goto
怎么了?
那么goto
有什么问题会导致很多问题呢? 在60年代后期, Essger Wieb Dijkstra撰写了两篇现已广为人知的著作,这有助于更清楚地理解这一点: 反对goto运算符的论点和关于结构化编程的注释 。
goto
破坏了抽象
在这些作品中,Dijkstra担心我们如何编写非平凡的程序并确保其正确性。 有很多有趣的观点。 例如,您可能听过以下短语:
测试程序可以显示错误的存在,但不能显示错误的存在。
是的,这是来自《 结构编程说明》 。 但是他主要关心的是抽象 。 他想编写太大的程序以至于无法掌握。 为此,您必须将程序的各个部分视为黑盒-例如,您在Python中看到此程序:
print("Hello World!")
而且您不需要了解print
所有详细信息(行格式,缓冲,跨平台差异等)。 您需要知道的是, print
以某种方式可以打印您传入的文本,您可以集中精力在这段代码中要做的事情。 Dijkstra希望语言支持这种抽象。
在这一点上,发明了块语法,诸如ALGOL之类的语言累积了约5种不同类型的控制语句:它们仍然具有顺序执行线程和goto
线程:

以及获得的条件,周期和函数调用:

您可以使用goto
来实现这些高级构造,这就是人们以前对它们的看法:作为便捷的快捷方式。 但是Dijkstra指出了goto
和其他控制运算符之间的巨大差异。 除了goto
,执行线程
我们可以将其称为“黑匣子规则”-如果控件结构(控件运算符)具有这种形式,则在您对内部细节不感兴趣的情况下,可以忽略“发生某些事情”部分,并将该块视为常规顺序团队。 更好的是,对于由这些块组成的任何代码都是如此。 当我看:
print("Hello World!")
为了了解执行线程的去向,我不需要阅读print
源及其所有依赖项。 也许在print
内部有一个循环,并且在其中有一个条件,其中有对另一个函数的调用...这都不重要-我知道线程将要print
,该函数将完成其工作,最终线程将返回我的代码我看了
但是,如果您拥有使用goto
的语言-一种功能和其他所有功能都基于goto
构建的语言,并且goto
可以随时随地跳转-那么这些结构根本就不是黑匣子! 如果您有一个带循环的函数,里面有一个条件,里面有goto
...,那么这个goto
可以将执行传递给任何地方。 也许控件会突然从您尚未调用的另一个函数中完全返回! 你不知道!
这就破坏了抽象- 任何函数都可以在内部具有潜在的goto
功能,而要找出是否存在这种情况的唯一方法就是牢记系统的所有源代码。 语言goto
,您将无法预测执行流程。 这就是为什么 goto
导致意大利面条式代码的原因 。
迪杰斯特拉一理解问题,便能够解决。 这是他的革命性假设-我们不应将条件/循环/函数调用视为goto
缩写,而应视为具有我们权利的基本原语-我们应该从我们的语言中完全删除goto
。
从2018年开始,这似乎很明显。 但是,当您尝试拿起他们不安全的玩具时,程序员有何反应? 1969年,迪杰斯特拉的提议似乎令人难以置信。 唐纳德·努斯(Donald Knuth) 为 goto
辩护 。 成为goto
编码专家的人们理所当然地感到愤慨,他们不必重新学习如何以更严格的新术语来表达自己的想法。 当然,还需要创建全新的语言。
结果,现代语言比Dijkstra最初的措辞严格一些。

左:传统的goto
。 右:驯化的goto
,如C,C#,Golang等。 未能越过职能界限意味着他仍然可以在您的鞋子上撒尿,但不太可能使您撕裂。
它们允许您使用break
, continue
或return
跳过结构控制语句的嵌套级别。 但是从根本上讲,它们都是围绕Dijkstra的想法构建的,并且可以严格限制地破坏顺序执行流程。 特别是,功能-将执行线程包装在黑匣子中的基本工具-是坚不可摧的。 您不能从一个功能执行break
命令到另一个功能,而return
不能使您返回比当前功能更远的位置。 函数内部执行线程的任何操作都不会影响其他函数。
保留goto
运算符的语言(C,C#,Golang等)严重限制了它。 至少,它们不允许您从一种功能的主体跳到另一种功能。 如果您没有使用Assembler [2],那么经典的无限goto
成为过去。 迪克斯特拉赢了。
没有goto的新世界
goto
的消失发生了一些有趣的事情-语言创建者能够根据结构化的执行流程开始添加新功能。
例如,Python具有自动清除资源的出色语法- 上下文管理器 。 您可以写:
这样可以确保在运行时打开文件,但需要some code
但之后要立即关闭。 大多数现代语言都具有等效功能( RAII , using
,try-with-resource, defer
,...)。 他们都假定控制流程是有序的。 如果我们使用goto
进入with
块会发生什么? 文件是否打开? 如果我们跳出那里而不是像往常一样离开?
块中的代码完成后,启动__exit__()
方法,该方法关闭打开的资源,例如文件和连接。
文件会关闭吗? 在goto
,上下文管理器根本无法以清晰的方式工作。
错误处理也存在同样的问题-发生错误时,代码应该怎么做? 通常-将错误的描述(调用的堆栈)发送到调用代码,并让其决定要做什么。 现代语言具有专门用于此的结构,例如Exception或其他形式的自动错误引发 。 但是,只有在该语言具有调用堆栈和可靠的“调用”概念的情况下,此帮助才可用。 在FLOW-MATIC示例的流示例中,回顾意大利面条,并想象一下中间抛出的异常。 哪有呢?
没有更多的
因此,传统的goto
(忽略函数边界)是不好的,这不仅是因为难以正确使用。 如果只有这样, goto
可能会留下来-仍然存在许多不良的语言构造。
但是,即使是该语言的goto
功能,也会使一切变得更加复杂。 第三方库不能被视为黑匣子-在不知道来源的情况下,您无法弄清楚哪些功能是正常的,哪些功能无法控制地控制执行流程。 这是预测本地代码行为的主要障碍。 上下文管理器和自动错误弹出窗口等强大功能也将丢失。 最好完全删除goto
,以支持支持黑匣子规则的控制运算符。
关于诸如“ Go”之类的表达的危险
因此,我们看了goto
故事。 但是它适用于go
运算符吗? 好吧...总而言之! 这个比喻是惊人的准确。
去表达式打破抽象。
还记得我们怎么说过,如果语言允许goto
,那么任何函数都可以自身隐藏goto
? 在大多数异步框架中, go
表达式会导致相同的问题-任何函数都可能(也可能不)在后台运行任务。 该函数似乎已返回控制权,但它仍在后台运行吗? 而且,如果不阅读函数的源代码及其调用的所有内容,便无法找到答案。 什么时候结束? 很难说。 如果您拥有go
及其类似物,则功能不再是尊重执行流程的黑匣子。 在我关于异步API的第一篇文章中,我将其称为“因果冲突”,并发现这是使用asyncio
和Twisted的程序中许多常见的实际问题的根本原因,例如流控制问题,正确关闭的问题等。
这是指控制进入和离开程序的数据流。 例如,该程序以3MB / s的速度接收数据,而以1MB / s的速度离开,因此该程序消耗越来越多的内存,请参阅作者的另一篇文章。
go-expressions破坏了开放资源的自动清理功能。
让我们再来看一个with
语句的示例:
之前我们说过,我们“保证”在some code
正常工作时打开文件,然后再关闭文件。 但是,如果some code
启动了后台任务怎么办? : , , with
, with
, , , . , ; , , some code
.
, , - , , .
, Python threading
— , , — , with
, , , ( ). , . , .
go- .
, , (exceptions), . " ". , . , , . , , … , . , - . ( , , " - " — ; .) Rust — , , - — . (thread) , Rust .
, , join , errbacks Twisted Promise.catch Javascript . , , . , Traceback . Promise.catch
.
, .
go
, goto
, go- — , , , . , goto
, , go
.
, , ! :
, Trio .
go
: , , , . , , :

, , , " " .
? " " ,
) , , ( ),
) , .
. , - . , .. [3]
: , , , "" , . Trio , async with
:

, as nursery
nursery
. nursery.start_soon()
, () : myfunc
anotherfunc
. . , , () , , .

, , — , , . , .
, .
:

, . 以下是其中一些:
.
go- — , , , . — , , . , , .
.
, . :
run_concurrently([myfunc, anotherfunc])
async.gather Python, Promise.all Javascript, ..
, , , . , accept
, .
accept
Trio:
async with trio.open_nursery() as nursery: while True: incoming_connection = await server_socket.accept() nursery.start_soon(connection_handler, incoming_connection)
, , run_concurrently
. , run_concurrently
— , , run_concurrently
, .
.
. , , ? : . , async with open_nursery()
nursery.start_soon()
, — [4], , , . , , .
, , " ", :
, .
, , go-, .
, .
, - . , , . :
async with my_supervisor_library.open_supervisor() as nursery_alike: nursery_alike.start_soon(...)
, , . .
Trio , asyncio
: start_soon()
, Future
( , Future
). , ( , Trio Future
!), .
, , .
, , — — .
Trio, . , , " " ( ), Cancelled
. , , — - , " ", , .. , , . , , , .
.
" ", with
. , with
, .
.
, , . .
Trio, , … - . , . , — " " — , myfunc
anotherfunc
, . , , .
, : (re-raise) , . ,
" " , , , , , .
, , . ?
— ( ) , . , , , , .
, , - ( task cancellation ). C# Golang, — .
go
goto
, with
; go
- . 例如:
- , , , . ( : - )
- — Python ,
ctrl-C
( ). , .
, . ?
… : ! , , . , , , break
continue
.
, . — , 1970 , goto
.
. (Knuth, 1974, .275):
, goto
, , " " goto
. goto
! , , goto
, . , , . , — , — "goto" .
: . , , . , , . , , .
, Happy Eyeballs ( RFC 8305 ), TCP . , — , , . Twisted — 600 Python . 15 . , , , . , , . , . ? . .
结论
— go
, , , Futures
, Promises
,… — goto
, . goto
, -- goto
, . , , ; , . , goto
, .
, , ( CTRL+C
) , .
, , , , — , goto
. FLOW-MATIC , , - . , , Trio , , .
留言
Trio .
:
Trio : https://trio.discourse.group/
致谢
Graydon Hoare, Quentin Pradet, and Hynek Schlawack . , , .
berez .
: FLOW-MATIC (PDF), .
Wolves in Action, Martin Pannier, CC-BY-SA 2.0 , .
, Daniel Borker, CC0 public domain dedication .
[2] WebAssembly , goto
: ,
[3] , , , , :
The "parallel composition" operator in Cooperating/Communicating Sequential Processes and Occam, the fork/join model, Erlang supervisors, Martin Sústrik's libdill , crossbeam::scope / rayon::scope Rust. golang.org/x/sync/errgroup github.com/oklog/run Golang.
, - .
[4] start_soon()
, , start_soon
, , , . , .
Nathaniel J. Smith , Ph.D., UC Berkeley numpy
, Python . Nathaniel .
:
, , , Haskell , , .
( , 0xd34df00d , ) , ( Happy Eyeballs ), .
, Trio ? Haskell Golang ?
: