关于并行计算的结构或反对“ Go”运算符的参数


每种支持并行(竞争性,异步)计算的语言都需要一种并行运行代码的方式。 以下是来自不同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库。 她没有使用这些方法。 相反,如果我们要并行运行myfuncanotherfunc ,我们将编写如下代码:


 async with trio.open_nursery() as nursery: nursery.start_soon(myfunc) nursery.start_soon(anotherfunc) 

托儿所-托儿所

第一次面对“托儿所”的设计,人们迷路了。 为什么会有上下文管理器 (带块)? 这是什么托儿所,为什么需要执行任务? 然后人们了解到托儿所会干扰其他框架中的常规方法并生气。 一切似乎都离奇,具体且过于高级,以至于不能成为基本的基本知识。 所有这些都是可以理解的反应! 但是要忍受一点。


在本文中,我想说服您,托儿所不是一种时尚,而是一种用于控制执行流程的新原语,就像循环和函数调用一样。 此外,上面讨论的方法(创建线程和注册回调)需要丢弃,并由托儿所代替。


听起来太大胆了吗? 但这已经发生了:一旦goto广泛用于控制程序的行为。 现在这是一个笑的机会:



几种语言仍然具有所谓的goto ,但是它的功能比原始的goto更为有限。 而且在大多数语言中根本不是。 他怎么了 这个故事令人惊讶地相关,尽管由于其古老而对大多数人都不熟悉。 让我们提醒自己goto什么,然后看看这对异步编程有何帮助。


目录


  • 什么是去?
  • 怎么了
  • 发生了什么事?
    • goto破坏了抽象
    • 没有goto的新世界
    • 没有更多的
  • 关于“ Go”类型表达式的危险
    • 去表达式打破抽象。
    • go-expressions破坏了开放资源的自动清理功能。
    • go表达式破坏错误处理。
    • 没有更多的去
  • 苗圃作为围棋的结构替代品
    • 托儿所保留功能的抽象。
    • 苗圃支持动态添加任务。
    • 您仍然可以离开托儿所。
    • 您可以识别可作为托儿所的新类型。
    • 不,但是,托儿所总是在等待内部所有任务的完成。
    • 可以自动清理资源。
    • 漏洞修复工程。
    • 勇往直前的新世界
  • 苗圃实践
  • 结论
  • 留言
  • 致谢
  • 脚注
  • 关于作者
  • 延续性

什么是goto


第一台计算机是使用assembler或什至更原始的方式进行编程的。 这不是很方便。 因此,在1950年代,诸如IBM的John BackusRemington Rand的 Grace Hopper之类的人开始开发诸如FORTRANFLOW-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相同的表达式。
  • 对于FuturesPromises同样的事情-当您运行该函数并返回Promise ,这意味着它计划在后台工作并返回一个控件对象以稍后获得结果(如果需要)。 从管理语义的角度来看,它与创建流程相同。 之后,将回调传递给Promis,然后如上一段所述。

这种相同的模式以多种形式显示出来-关键的相似之处在于,在所有这些情况下,控制流都是分开的-跳转到新线程,但是父线程返回到调用它的线程。 知道要看什么,您到处都会看到它! 这是一个有趣的游戏(至少对于某些类型的人而言)!


仍然令我烦恼的是,此类控制语句没有标准名称。 我使用表达式“ go”来称呼它们,就像“ goto”已成为所有goto表达式的通用术语goto 。 为什么go ? 原因之一是Golang为我们提供了这种语法的非常干净的示例。 另一个是:



注意到相似之处吗? 是的gotogoto形式之一。


异步程序因编写和分析的困难而臭名昭著。 以及基于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等。 未能越过职能界限意味着他仍然可以在您的鞋子上撒尿,但不太可能使您撕裂。

它们允许您使用breakcontinuereturn跳过结构控制语句的嵌套级别。 但是从根本上讲,它们都是围绕Dijkstra的想法构建的,并且可以严格限制地破坏顺序执行流程。 特别是,功能-将执行线程包装在黑匣子中的基本工具-是坚不可摧的。 您不能从一个功能执行break命令到另一个功能,而return不能使您返回比当前功能更远的位置。 函数内部执行线程的任何操作都不会影响其他函数。


保留goto运算符的语言(C,C#,Golang等)严重限制了它。 至少,它们不允许您从一种功能的主体跳到另一种功能。 如果您没有使用Assembler [2],那么经典的无限goto成为过去。 迪克斯特拉赢了。


没有goto的新世界


goto的消失发生了一些有趣的事情-语言创建者能够根据结构化的执行流程开始添加新功能。


例如,Python具有自动清除资源的出色语法- 上下文管理器 。 您可以写:


 # Python with open("my-file") as file_handle: some code 

这样可以确保在运行时打开文件,但需要some code但之后要立即关闭。 大多数现代语言都具有等效功能( RAIIusing ,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语句的示例:


 # Python with open("my-file") as file_handle: some code 

之前我们说过,我们“保证”在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 .


, , ! :


  • go -, , " ",
  • , 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 ?


:

Source: https://habr.com/ru/post/zh-CN479186/


All Articles