關於結構化並發的筆記 —— Go 語言中有害的聲明語句

來源:互聯網
上載者:User
## 前言每一個並發的 API 背後的代碼都需要允許並發啟動並執行,以下是使用不同 API 的例子:```go myfunc(); // Golangpthread_create(&thread_id, NULL, &myfunc); /* C with POSIX threads */spawn(modulename, myfuncname, []) % Erlangthreading.Thread(target=myfunc).start() # Python with threadsasyncio.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; // Javascriptpromise.then(myfunc, errorhandler) // Javascript with Promisesdeferred.addCallback(myfunc) # Python with Twistedfuture.add_done_callback(myfunc) # Python with asyncio```再說,雖然文法不一樣,但是它們都完成同樣的事情:它們安排好任務(arrange),之後,直到某一事件發生了,myfunc 就會運行。註冊“事件回調”成功,以上的函數就立即返回,調用者可以繼續做其他事情。(有時候回調可以被巧妙地封裝成 helper,例如 [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) [combinators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race),或者 [Twisted-style protocols/transports](https://twistedmatrix.com/documents/current/core/howto/servers.html) ,但核心思路是一樣的)還有其他方式嗎?你使用的任何的現實上的並發的 API ,你可能會發現他們都是殊途同歸的,例如 python 的 asyncio 。但我的新開發的庫 [Trio](https://trio.readthedocs.io/) 與眾不同,它沒有有使用其他方式。相反,如果我們希望並發執行 `myfunc` 或者其他函數,我們可以這樣寫:```pythonasync with trio.open_nursery() as nursery: nursery.start_soon(myfunc) nursery.start_soon(anotherfunc)```當人們遇到 `nursery` 的構建方法時,他們想瞭解它的神秘之處,為什麼有一個縮排?為什麼我需要一個 `nursery` 對象來派發非同步任務,人們開始感覺這有違以往的使用習慣,感到厭煩,這個庫讓人感到怪異,遠遠脫離原始的文法,這些都是可以理解的反應,但是請諒解我。在這篇文章,我希望說服你 `nurseries` 並不總是怪異特殊的,而是一個新的控制流程原語,它與迴圈或函數調用一樣重要。此外,我們在上面看到的其他方法 - 線程派發和回調註冊 - 應該被完全移除並替換為 `nurseries` 。聽起來難以接受?曆史發生過類似的事: 語句 `goto` 曾一度認為是控制流程中的王者,現在依舊淪落為 [過去式](https://xkcd.com/292/) 。一些語言依舊有類似 `goto` 的語句,相比原來的 `goto` 有所不同或者被弱化。大部分語言甚至沒有它。發生了什嗎?時間太長久以至於人們忘記了過去的故事,但是結果令人驚訝的類似。所以我們首先會提醒自己什麼是goto,然後看看,關於並發API,它可以教給我們的東西。## 大綱目錄- 到底什麼是 `goto`- 到底什麼是 `go`- `goto` 發生了什麼 1. `goto` 抽象的毀滅者 2. 一個驚喜,移除 `goto` 語句帶來新的特性 3. `goto` 語句:不再使用- `go` 語句被認為有害 1. go 語句:不再使用- Nurseries: 一個替代 `go` 語句的構件 1. Nurseries 支援函數抽象 2. Nurseries 支援動態任務派發 3. 那有一個逃逸 4. 你可以定義一個像 nursery 的新類型 5. 不,事實上,nurseries 總是等待裡面的任務退出 6. 自動清理資源的工作 7. 自動傳遞錯誤資訊的工作 8. 一個令人驚訝的好處:移除 `go` 語句開啟一個新的特性- 實踐 Nurseries- 結論- 致謝- 腳註## `goto` 到底是什麼讓我們回顧曆史:早期的電腦是使用組合語言編程的,或者其他更原始的機器機制。有些簡陋。因此,在20世紀50年代,IBM 的 John Backus 和 Remington Rand 的 Grace Hopper 等人開始開發 FORTRAN 和 FLOW-MATIC 等語言(以其直接後繼 COBOL 而聞名)。FLOW-MATIC當時非常雄心勃勃。你可以把它看作是Python的曾曾曾祖父母:第一種語言,首先為人類設計,其次是電腦。以下是一些FLOW-MATIC代碼,讓您體驗它的外觀:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/flow-matic-code-0.png)你注意到它不像現代語言,沒有 `if` 程式碼片段,`loop` 迴圈語句,或者 function 函數調用,實際上根本沒有塊分隔字元或縮排。這隻是一個簡單的語句(操作符)列表。並不是因為這個程式太短而無法使用更進階的控制文法,而是因為”塊文法“還沒有發明出來!![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/compare-sequential-to-goto.png)反而, FLOW-MATIC 有兩種控制流程的方式,比較通常的是順序(sequential),如你所想,從頭到尾一句一句地、串列地執行語句,但如果你執行一句特殊的語句 `JUMP TO`,它會立馬直接跳轉到其他控制語句。例如,的語句13跳轉到語句2:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/flow-matic-code-1.png)就好像一開始的並發原語,對於如何稱呼這個“進行跳轉動作”的操作,有一些意見分歧。這裡是 `JUMP TO` ,但是這裡叫做 `goto` (就好像 "go to"),於是我們在這裡使用這個稱呼。這個小程式使用的完整的 `goto` 跳轉路徑:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/flow-matic-code-2.png)如果你感到這看起來很費解,你不是一個人!這個跳轉風格的程式是 `FLOW-MATIC` 非常直接地繼承組合語言而來。這很功能強大,非常適合電腦硬體的實際工作方式,但直接使用會讓人感到非常困惑。亂七八糟的箭頭讓人發明了“意大利麵代碼”這個詞。顯然,我們需要更好的東西。但是... goto導致所有這些問題的關鍵是什嗎?為什麼有的控制結構好,有些不是?我們如何選擇好的?這時,這個問題真的很不清楚,如果你不瞭解問題,很難解決問題。## `go` 到底是什麼但讓我們暫停回顧曆史 - 每個人都知道 `goto` 是不好的。這與並發性有什麼關係?那麼,考慮Go語言的出名的 `go` 語句,用於產生一個新的 `goroutine` (輕量級線程):```golang// Golanggo myfunc();```我們可以繪製其控制流程程圖嗎?這不同於我們上面看到,因為控制流程實際上是分裂的。我們可以這樣畫:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/go-myfunc.png)這裡的顏色被分成兩條路徑。從主線(綠線)的角度來看,控制流程按順序走:它出現在頂部,然後立即出現在底部。與此同時,從支線(淡紫色線)的角度看,控制流程進入頂部,然後跳轉到myfunc的主體。與常規函數調用不同,這種跳轉是單向的:當運行myfunc時,我們切換到一個全新的堆棧,並且運行時立即忘記了我們來自哪裡。但這不僅適用於Golang。這是我們在本文開頭列出的所有基元的流程式控制製圖:- 線程庫通常提供某種類型的控制代碼對象,讓您稍後可以加入線程 - 但這是一種語言不知道的獨立操作。實際的線程產生原語具有上面顯示的控制流程程。- 註冊回調在語義上等同於啟動一個後台線程,該後台線程(a)阻塞,直到發生某個事件,然後(b)運行回調。(雖然顯然實現是不同的)因此,就進階別控制流程而言,註冊回調本質上是一種 go 語句。- `future` 和 `promise` 也是一樣的:當你調用一個函數並且它返回一個 promise 時,這意味著它計劃在後台發生的工作,然後給你一個控制代碼對象以後加入工作(如果你想的話)。就控制流程語義而言,這就像產生一個線程一樣。然後你在這個 promise 上註冊回調,所以看到前面的要點。同樣的確切模式以許多形式出現:關鍵的相似之處在於,在所有這些情況下,控制流程分離,一方進行單向跳轉,另一方返回給調用者。一旦你知道要尋找什麼,你就會開始在各地看到它 - 這是一個有趣的遊戲![1]令人煩惱的是,這類控制流程結構沒有標準名稱。因此,就像 `goto語句` 成為所有不同類似 goto 結構的總稱一樣,我將使用 `go語句` 作為這些術語的總稱。為什麼這麼做? 一個原因是 Go 語言給了我們一個特別純粹的形式例子。另一個是......好吧,你可能已經猜到了我說什麼了。看看這兩個圖。注意任何相似之處:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/compare-go-to-goto.png.png)沒錯:go語句是goto語句的一種形式。並發程式的編寫和推理是非常困難的。基於goto的程式也是如此。這可能是出於某些相同的原因嗎?在現代語言中,goto引起的問題在很大程度上得到解決。如果我們從 ”研究他們如何修正“ 轉到 ”它會教我們如何製作更多可用的並發API“,讓我們來找出答案。## goto發生了什麼事?那麼,為什麼 goto 會導致如此多的問題?在二十世紀六十年代後期,Edsger W. Dijkstra 編寫了兩篇很著名的論文,協助人們更清楚地瞭解這一點:[goto 語句被認為有害](https://scholar.google.com/scholar?cluster=15335993203437612903&hl=en&as_sdt=0,5) 與 [結構化編程的筆記](https://www.cs.utexas.edu/~EWD/ewd02xx/EWD249.PDF)### goto:抽象的毀滅者在這些論文中,Dijkstra擔心如何編寫非凡的軟體並使其正確。我無法在這裡詳細說明。例如,你可能聽說過這句話:> 程式測試可以用來發現 bug 的存在,但卻沒法證明 bug 不存在是的,這來自[結構化編程的筆記](https://www.cs.utexas.edu/~EWD/ewd02xx/EWD249.PDF)。但他主要關心的是抽象。他想寫一些太大的項目,不能一下子把想到的都寫下來。要做到這一點,您需要將程式的某些部分像黑盒子一樣對待 - 就像當您看到 Python 程式時一樣:```pythonprint("Hello world!")```那麼你不需要知道列印是如何?的(字串格式化,緩衝,跨平台差異......)的所有細節。您只需要知道它會以某種方式列印您提供的文本,然後您可以花費精力去考慮您的代碼中是否希望在此時發生這種情況。 Dijkstra希望語言支援這種抽象。至此,塊文法已經被發明出來,像ALGOL這樣的語言已經積累了5種不同類型的控制結構:它們仍然具有順序流和 `goto` :![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/compare-sequential-to-goto.png)並且還獲得了if / else,迴圈和函數調用的變體:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/if-loop-functioncall.png)你可以使用goto來實現這些更高層次的結構,並且在早期,人們就會想到它們:作為一個方便的簡寫。但是Dijkstra指出的是,如果你看這些圖表,goto和其他人之間有很大的區別。對於除goto以外的所有內容,流量控制位於頂部→[有問題]→流量控制位於底部。我們可以稱之為“黑盒子規則”:如果一個控制結構具有這種形狀,那麼在你不關心內部發生的細節的上下文中,你可以忽略[stuff happen]部分,並把整個作為規則的順序流程。而且更好的是,任何由這些部分組成的代碼也是如此。當我看這個代碼時:```pythonprint("Hello world!")```我不必去閱讀 `print` 及其所有傳遞依賴的定義,只是想知道控制流程程是如何工作的。也許在內部 `print` 有一個迴圈,並且在迴圈內部有一個 `if / else` ,並且在 `if / else` 內部還有另一個函數調用......或者也可能是其他內容。它並不重要:我知道控制將流入 `print` ,函數將完成它的事情,然後最終控制將返回到我正在閱讀的代碼。看起來這很明顯,但如果你有一個帶有 `goto` 的語言 —— 一種語言,其功能和其他所有內容都建立在 `goto` 之上,`goto` 可以隨時隨地跳轉 - 然後這些控制結構根本不是黑匣子!如果你有一個函數,並且在函數內部有一個迴圈,並且在迴圈內部有一個`if/else`,並且在 `if/else` 中有一個 `goto` ...那麼 `goto` 可以將控制發送到任何它想要的地方。也許控制會突然從另一個你還沒有調用的函數完全返回,你不知道!這就打破了抽象:這意味著每一個函數調用都可能是一個變相的 `goto` 語句,唯一需要知道的就是將系統的整個原始碼一次性儲存在頭腦中。只要 `goto` 使用你的語言,你就會停止對流量控制進行本地推理。這就是為什麼 `goto` 會導致意大利麵代碼。現在 Dijkstra 明白了這個問題,他能夠解決這個問題。這是他的革命性建議:我們應該停止將 `if / loops /function call` 作為 `goto` 的簡寫,而應該將它們作為自己權利的基本原語 - 並且我們應該完全從我們的語言中刪除 `goto`。從2018年起,這似乎顯而易見。但是當你試圖拿走他們的玩具時,你有沒有看過程式員的反應,因為他們不夠聰明,無法安全使用它們?是的,有些事情永遠不會改變。 1969年,這個提議令人難以置信地引起爭議。[Donald Knuth](https://en.wikipedia.org/wiki/Donald_Knuth) 為 `goto` [辯護](https://scholar.google.com/scholar?cluster=17147143327681396418&hl=en&as_sdt=0,5) 。曾經成為編寫代碼專家的人非常不滿,他們基本上不得不基本學會如何重新編程,以便使用更新,更有約束的構造來表達自己的想法。當然,它需要建立一套全新的語言。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/goto-change.png)最後,現代語言比 Dijkstra 的原始公式稍遜一籌。他們會讓你使用 `break`,`continue` 或 `return` 等構造立即跳出多個嵌套結構。但基本上,他們都是圍繞 Dijkstra 的想法而設計的;即使這些推動邊界的構造只能以嚴格有限的方式進行。特別是,`function` - 這是在黑匣子內部封裝控制流程程的基本工具 - 被認為是不可侵犯的。你不能從一個函數中跳出另一個函數,並且返回可以將你從當前函數中取出,但不能再進一步。無論控制流程程如何,一個函數在內部起作用,其他函數不必關心。這甚至延伸到 goto 本身。你會發現幾種語言仍然有一些他們稱之為 goto 的語言,比如 C,C#,Golang ......但是它們增加了很多限制。至少,他們不會讓你跳出一個 function 並跳入另一個 function 。除非你在彙編[2]中工作,無限制的goto不見了。 Dijkstra贏了。### 一個驚喜,移除 `goto` 語句帶來新的特性一旦 goto 消失,就會發生一些有趣的事情:語言設計者能夠開始添加依賴於控制流程程結構的功能。例如,Python有一些很好的資源清理文法: with 語句。你可以寫下如下內容:```python# Pythonwith open("my-file") as file_handle: ...```並保證該檔案將在 `...` 代碼期間開啟,但隨後會立即關閉。大多數現代語言都有一些等效的(RAII,使用,試用資源,延遲 ......)。他們都假設控制流程程是有序的,結構化的。如果我們使用goto語句跳入我們中間有塊 `...` 你會怎麼辦?檔案是否開啟?如果我們再次跳出來,而不是正常退出?檔案會關閉嗎?此功能只是沒有任何連貫的方式工作。錯誤處理有類似的問題:當出現問題時,你的代碼應該做什嗎?答案常常是把堆棧中的堆棧傳遞給代碼的調用者,讓他們弄清楚如何處理它。現代語言具有專門的構造來使這更容易,例如異常或其他形式的[自動錯誤傳播](https://doc.rust-lang.org/std/result/index.html#the-question-mark-operator-)。但是你的語言只能提供這種協助,如果它有一個堆棧和一個可靠的“呼叫者”概念。再看一下我們FLOW-MATIC程式中的控制流程麵條,並想象在它的中間試圖引發異常。它甚至會去哪裡?### goto 語句:不再使用所以 `goto` —— 忽略函數界限的傳統類型 —— 不僅僅是一種常見的壞特性,難以正確使用。如果僅僅如此,它可能會保留下來。但實際情況更糟。即使你不使用 goto ,只要把它作為你的語言的一個選項,就會讓所有的東西都難以使用。每當你開始使用第三方庫時,你都不能把它當作一個黑匣子 - 你必須仔細閱讀它以找出哪些函數是常規函數,哪些函數是偽裝的特殊控制結構流。這是本地推斷的嚴重障礙。你失去了強大的語言功能,如可靠的資源清理和自動錯誤傳遞。我們應該更好地完全移除goto,以支援遵循“黑匣子”規則的控制流程構造。### go 語句被認為有害所以這就是goto的曆史。現在,這多少適用於 `go`語句? 那麼......基本上,所有這一切!這個比喻結果令人震驚。Go語句打破了抽象。請記住我們如何說如果我們的語言允許跳轉,那麼任何功能都可能是變相跳轉?在大多數並發架構中,go語句會導致完全相同的問題:無論何時調用函數,它都可能會或可能不會產生一些背景工作。該功能似乎回來了,但它仍然在後台運行?如果沒有閱讀所有的原始碼,就沒有辦法知道。何時完成?很難說。如果你有 go 語句,然後功能不再相對於黑盒子來控制流程量。在我的第一篇關於並發API的文章中,我稱之為“違反因果關係”,並發現它是使用 asyncio 和 Twisted 的程式中的許多常見現實問題的根源,例如背壓問題,正確關閉問題等等。Go語句會中斷自動資源清理。讓我們再一次`with` 語句樣本:```python# Pythonwith open("my-file") as file_handle: ...```之前,我們說我們是保證(guaranteed)的同時,該檔案將被開啟、代碼運行,然後完成關閉。但是,是否有東西可以讓代碼產生一個背景工作?那麼我們的保證就失去了:該操作看起來 `with` 塊的內部操作會在 `with` 塊結束之後被回收,因為檔案被同時他們還在使用它關閉。再次,你不能從當地的檢查中看出來; 要知道,如果發生這種情況,你必須去閱讀原始碼,看內部實現功能的 `...` 代碼。如果我們希望此代碼正常工作,我們需要以某種方式跟蹤任何背景工作,並且只有在完成後手動安排檔案才能關閉。這是可行的 - 除非我們正在使用一些不提供任何方式在任務完成時得到通知的庫,這是非常常見的(例如因為它沒有公開任何可以加入的任務控制代碼)。但即使在最好的情況下,非結構化的控制流程程也意味著語言無法協助我們。我們現在回到實施資源清理手中,就像在過去的糟糕時期。Go語句打破錯誤處理。就像我們上面討論的那樣,現代語言提供了強大的工具,例如異常,以協助我們確保檢測到錯誤並將其傳播到正確的位置。但是這些工具依賴於擁有“當前代碼的調用者”的可靠概念。只要您產生任務或註冊回調,該概念就會被破壞。結果,我所知道的每個主流並發架構都簡單地放棄了。如果背景工作發生錯誤,而您沒有手動處理它,那麼運行時只是......將它放到地板上並不再管它,這不是太重要。如果幸運的話,它可能會在控制台上列印一些東西。(我唯一使用過的認為“列印某些內容並繼續前進”的軟體是一個很好的錯誤處理策略,它是古老的Fortran庫,但我們在這裡。)甚至 Rust - 這門語言在高中階段被投票選為“正確度最高的人”。如果後台線程發生混亂,Rust [拋棄錯誤並期望變得更好](https://doc.rust-lang.org/std/thread/) 。當然,您可以在這些系統中正確處理錯誤,仔細確保加入每個線程,或者通過構建自己的錯誤傳播機制,如 [Javascript中的Twisted](https://twistedmatrix.com/documents/current/core/howto/defer.html#visual-explanation) 或 [Promise.catch中的errbacks](https://hackernoon.com/promises-and-error-handling-4a11af37cb0e) 。但是現在你正在編寫一個特殊的,脆弱的重新實現你的語言已經有的功能。你失去了諸如“回溯”和“調試器”等有用的東西。所需要的只是忘記撥打 `Promise.catch` 一次,突然間,您甚至沒有意識到地板上的嚴重錯誤。即使你以某種方式解決了所有這些問題,你仍然會得到兩個冗餘系統來做同樣的事情。### go 語句:不再使用就像goto是第一批實用的進階語言的明顯原始代碼一樣,go是第一個實用並發架構的明顯原語:它匹配底層發送器實際工作的方式,並且它足夠強大,可以實現任何其他並發流模式。但是,再次像goto一樣,它打破了控制流程抽象,所以只是將它作為您的語言的一個選項使得一切都變得更難以使用。好訊息是,這些問題都可以解決,Dijkstra 向我們展示如何做到:- 找到具有類似功能的語句的替代品,但遵循“黑匣子規則”,- 將這個新構造作為原語構建到我們的並發架構中,並且不包含任何形式的go語句。這就是 Trio 所做的。### Nurseries: 一個替代 `go` 語句的構件以下是核心思想:每次我們的控制分裂成多個並發路徑時,我們都要確保他們再次歸納起來。例如,如果我們想同時做三件事情,我們的控制流程程應該如下所示:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/control-flow.png)注意,這隻有一個箭頭出現在頂部,一個出現在底部,所以它遵循 Dijkstra 的黑盒子規則。現在,我們怎樣才能把這個草圖變成一個具體的語言結構呢?有一些現有的構造可以滿足這個約束,但是(a)我的提議與我所知道的並且比它們有優勢(特別是在想要使其成為獨立原語的情況下)略有不同,並且(b)並發性是龐大而複雜的,試圖把所有的曆史和權衡分開將會使論證完全失敗,所以我將把它延遲到另一篇單獨的文章。在這裡,我只關註解釋我的解決方案。但請注意,我並不是說自己喜歡,發明了並發或某種東西,我是站在巨人的肩膀上,從很多來源吸取靈感。無論如何,下面是我們要做的事情:首先,我們聲明父任務不能啟動任何子任務,除非它首先為子任務建立一個地方: Nurseries 。它通過開啟一個 Nurseries 塊來實現這一點; 在Trio中,我們使用 Python 的 `async with` 文法來執行此操作:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/python-async-with.png)開啟一個nursery塊會自動建立一個代表這個 Nurseries 的對象,並且 nursery 文法將這個對象賦給名為 nursery 的變數。然後我們可以使用 nursery 對象的 start_soon 方法來啟動並發任務:在這種情況下,一個任務調用函數 myfunc,另一個調用函數 anotherfunc。從概念上講,這些任務在 Nurseries 區內執行。實際上,將 nursery 塊內寫入的代碼視為建立塊時自動啟動的初始任務通常很方便。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/python-nursery.png)最重要的是,在 Nurseries 區塊內的所有任務都已經退出之前, Nurseries 區塊不會退出 - 如果父任務在所有子任務完成之前到達該區塊的末尾,那麼它會在那裡暫停並等待它們。 Nurseries 自動擴充以容納子任務。以下是控制流程程:您可以看到它與我們在本節開頭展示的基本模式的匹配情況:![](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-statement-considered-harmful/python-nursery-control-flow.png)這種設計有許多後果,並非全部都是顯而易見的。我們來看看其中的一些。### Nurseries 支援函數抽象go 語句的基本問題是,當你調用一個函數時,你不知道它是否會產生一些背景工作,在完成後繼續運行。使用 Nurseries ,您不必擔心這一點:任何函數都可以開啟 Nurseries 並運行多個並發任務,但函數只有在完成後才能返回。所以當一個函數返回時,你知道它確實完成了。### Nurseries 支援動態任務派發這是一個更簡單的原型,它也滿足我們上面的流程式控制製圖。它需要一個thunk的列表,並且同時運行它們:```pythonrun_concurrently([myfunc, anotherfunc])```但問題在於你必須知道你將要啟動並執行任務的完整列表,而這並非總是如此。例如,伺服器程式通常具有接受迴圈,它接收傳入的串連並開始一個新的任務來處理它們中的每一個。這是Trio中最小的接受迴圈:```pythonasync with trio.open_nursery() as nursery: while True: incoming_connection = await server_socket.accept() nursery.start_soon(connection_handler, incoming_connection)```有了 Nurseries ,這是微不足道的,但使用實現它 run_concurrently 會比較尷尬。如果你願意,可以很容易地在 nurseries 之上實現 run_concurrently - 但這並不是必須的,因為在 run_concurrently 可以處理的簡單情況下 ,nursery符號就像可讀的一樣。### 有一個逃逸 Nurseries 對象也給我們一個逃逸的出口。如果你確實需要編寫一個產生背景任務的函數,那麼背景任務會超出函數本身呢?這也很容易:通過功能一個 Nurseries 對象。直接在open_nursery()塊內非同步代碼調用 nursery.start_soon - 只要nursery塊保持開啟 [4],那麼任何獲得對該 Nurseries 對象的引用的人都可以獲得派發任務的能力進入那個 Nurseries 。你可以將它作為函數參數傳遞,通過隊列發送。在實踐中,這意味著你可以編寫“違反規則”的函數,但在以下限制內:- 由於 Nurseries 對象必須明確地通過,您可以立即通過查看其呼叫網站來識別哪些功能違反正常的控制流程,因此本地推理仍然是可能的。- 函數產生的任何任務仍然受到傳入的 Nurseries 的生命週期的約束。- 調用代碼只能通過它自己有權訪問的 Nurseries 對象。所以這與傳統模式仍然有很大的不同,任何時候任何代碼都可以在無限的生命週期內產生背景任務。有一個地方很有用就是證明 Nurseries 具有相當的表達能力去發表聲明,但是這篇文章已經足夠長了,所以我會再留到下一篇。### 你可以定義一個像 nursery 的新類型標準的 Nurseries 語義提供了一個堅實的基礎,但有時候你想要一些不同的東西。也許你會羨慕 Erlang ,並且想要定義一個類似 Nurseries 的類,通過重新啟動子任務來處理異常。這完全有可能,對於你的使用者來說,它看起來就像一個普通的 Nurseries :```pythonasync with my_supervisor_library.open_supervisor() as nursery_alike: nursery_alike.start_soon(...)```如果您有一個將 nursery 作為參數的函數,那麼您可以將其中的一個傳遞給它,以便為其產生的任務控制錯誤處理策略。相當漂亮。但是這裡有一個微妙之處,它推動 Trio 走向不同的約定,而不是 asyncio 或其他庫:這意味著 start_soon 必須採用一個函數,而不是協程對象或未來。(你可以多次調用一個函數,但是沒有辦法重啟一個協程對象或 Future。)我認為這是一個更好的約定,無論如何由於多種原因(特別是因為 Trio 甚至沒有 Future!),但仍值得一提。### 不,實際上, Nurseries 總是等待裡面的任務退出。關於任務取消和任務加入如何相互作用也值得討論,因為這裡有一些微妙之處 - 如果處理不正確 - 就打破了 Nurseries 不變式。在Trio中,代碼可能隨時收到取消請求。請求取消之後,下一次代碼執行“檢查點”操作([詳細資料](https://trio.readthedocs.io/en/latest/reference-core.html#checkpoints))時,會引發取消異常。這意味著請求取消與實際 發生時間之間存在差距- 任務執行檢查點之前可能需要一段時間,然後異常必須展開堆棧,運行清理處理常式等。發生時, Nurseries 總是等待全面清理。我們永遠不會終止任務,也不會讓它有機會運行清理處理常式,而我們永遠不會 即使在 Nurseries 正在取消的過程中,也可以讓任務在 Nurseries 外無人監管。### 自動清理資源的工作由於 Nurseries 按照黑匣子規則,它讓 `with` 塊重新工作。禁止這樣的行為:代碼塊結束時依舊有背景工作在執行。### 自動傳遞錯誤資訊的工作如上所述,在大多數並發系統中,背景工作中未處理的錯誤只是被丟棄,來不及做別的事。在 Trio 中,由於每項任務都在 Nurseries 內,每個 Nurseries 都是父任務的一部分,父任務需要等待 Nurseries 內的任務......我們確實有一些事情可以通過未處理的錯誤來完成。如果背景工作以異常終止,我們可以在父任務中重新拋出它。這裡的直覺是,一個 Nurseries 就像一個“並發呼叫”原語:我們可以將上面的例子看作是同時調用 myfunc 和 anotherfunc ,所以我們的調用棧已經變成了一棵樹。異常將這個調用樹傳播給根,就像它們傳播一個普通的調用棧一樣。這裡有一個細微之處:當我們在父任務中重新引發異常時,它將開始在父任務中傳播。通常,這意味著父任務將退出 Nurseries 區塊。但是我們已經說過,在任務仍在啟動並執行情況下,父任務不能離開 Nurseries 區塊。那麼我們該怎麼辦?答案是,當子任務發生未處理的異常時,Trio 立即取消同一 Nurseries 中的所有其他任務,然後等待它們完成後再重新引發異常。這異常直接導致導致堆棧釋放,如果我們想要釋放我們的堆棧樹中的一個分支點,我們需要展開其他分支,取消它們。這確實意味著如果你想用你的語言來實施 Nurseries,你可能需要在 Nurseries 代碼和你的取消系統之間進行某種整合。如果你使用像 C# 或 Golang 這樣的語言,這可能會非常棘手,通常通過手動對象傳遞和約定來管理取消,或者(更糟的是)沒有通用的取消機制的取消。### 一個令人驚訝的好處:移除 `go` 語句開啟一個新的特性消除 goto 使得以前的語言設計師能夠對程式結構做出更強的假設,從而實現了塊和例外等新功能; 消除 go 語句也有類似的效果。例如:- Trio 的登出系統(cancellation system)比競爭者更容易使用和更可靠,因為它可以假定任務嵌套在常規樹形結構中; 請參閱 [完逾時和人為取消](https://vorpus.org/blog/timeouts-and-cancellation-for-humans/) 。- Trio 是唯一的 Python 並發庫,其中 control-C 以 Python 開發人員期望的方式工作([詳細資料](https://vorpus.org/blog/control-c-handling-in-python-and-trio/))。Nurseries 提供可靠的機制來處理異常。## 實踐 Nurseries這就是理論。它在實踐中如何工作?這是一個實踐問題:你應該嘗試一下並找出答案!但是,嚴重的是,我們經曆過問題才明白過來。在這一點上,我非常確信基礎是健全的,但是也許我們會意識到我們需要做一些調整,比如早期的結構化編程倡導者最終如何從消除 `break` 和 `continue` 中得到回應。如果你是一位經驗豐富的並發程式員,他們只是學習Trio,那麼你應該預料到這需要習慣它。你將不得不學習新的方法來做事情 - 就像在20世紀70年代的程式員一樣,學習如何在沒有 `goto` 下編寫代碼是一個挑戰。但當然,這是關鍵。正如Knuth所寫的(Knuth,1974,p.275):> 也許最糟糕的錯誤任何一個可以相對於標的做出去報表是假設“結構化編程”是通過編寫程式來實現,因為我們總是有,然後消除去的。大部分去的不應該在那裡!我們真正需要的是這樣一種方式,我們很少甚至設想我們的計劃想約去 陳述,因為他們真正需要的幾乎沒有出現。我們表達思想的語言對我們的思維過程有著強烈的影響。因此,迪克斯特拉要求更多新的語言特徵 - 鼓勵清晰思考的結構 - 以避免這樣做對併發症的誘惑。到目前為止,這是我使用 Nurseries 的經驗:它鼓勵清晰的思維。它導致設計更加健壯,更便於使用,而且更好。而這些限制實際上使解決問題變得更容易,因為您花費更少的時間去嘗試不必要的複雜問題。在一個非常真實的意義上,使用 Trio 已經教會我成為一個更好的程式員。例如,考慮 Happy Eyeballs 演算法([RFC 8305](https://tools.ietf.org/html/rfc8305)),這是一種簡單的並發演算法,用於加快建立TCP串連。從概念上來說,演算法並不複雜 - 您嘗試了多次串連嘗試,並且為了避免網路過載而採用錯開的方式。但是如果你看看[Twisted的最佳實現](https://github.com/twisted/twisted/compare/trunk...glyph:statemachine-hostnameendpoint),它幾乎有600行Python,並且仍然[至少有一個邏輯錯誤](https://twistedmatrix.com/trac/ticket/9345)。 比起 Trio 的同類型項目縮短了15倍以上。更重要的是,使用Trio,我可以在幾分鐘內寫出它,而不是幾個月,而且我在第一次嘗試時就得到了正確的邏輯。我從來不可能在其他任何架構中做到這一點,即使我有更多的經驗。有關更多詳細資料,您可以觀看 [我上個月在Pyninsula的演講](https://www.youtube.com/watch?v=i-R704I8ySE)。這隻是個例嗎?時間會證明一切,它充滿著希望。## 結論流行的並發原語 - go 語句, thread spawning functions, callbacks, futures, promises 等等,它們都是 goto 的變體,理論上和實踐上都是如此。即使是現代化的馴化過的 `goto`,但舊的 `goto` 是可以跳出函數邊界的。即使我們不直接使用它們,這些原語也是危險的,因為它們破壞了我們推理控制流程的能力,並且從抽象的模組化部分組成複雜的系統,並幹擾了自動資源清理和錯誤傳播等有用的語言功能。因此,像 goto 一樣,他們在現代進階語言中沒有地位。 Nurseries 提供了一種安全方便的替代方案,保留了您語言的全部功能,實現了強大的新功能(如 Trio 的取消範圍和控制 C 處理所證明的),並且可以顯著提高可讀性,生產力和正確性。不幸的是,要完全享受這些好處,我們需要完全刪除舊的基元,這可能需要從頭開始構建新的並發架構 - 就像消除 設計新語言所需的 goto 一樣。但與 FLOW-MATIC 相比,它的表現令人印象深刻,我們大多數人都很高興我們已經升級到了更好的產品。我不認為我們會後悔切換到 Nurseries,Trio 也證明這是一個實用的通用並發架構的可行設計。## 致謝非常感謝 Graydon Hoare,Quentin Pradet 和 Hynek Schlawack 對本文的草稿提出的意見。任何剩餘的錯誤當然都是我的錯。## 參考文獻- FLOW-MATIC 範例程式碼來自 [this brochure(pdf)](http://archive.computerhistory.org/resources/text/Remington_Rand/Univac.Flowmatic.1957.102646140.pdf) 儲存在 [Computer History Museum](http://www.computerhistory.org/collections/catalog/102646140)- [Wolves in Action](https://www.flickr.com/photos/iam_photo/478178221) by i:am. photography / Martin Pannier, licensed under [CC-BY-SA 2.0](https://creativecommons.org/licenses/by-nc-sa/2.0/)- [French Bulldog Pet Dog](https://pixabay.com/en/french-bulldog-pet-dog-funny-2427629/) by Daniel Borker, 登載在 [CC0 public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/)

via: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/

作者:Nathaniel J. Smith 譯者:lightfish-zhang 校對:polaris1119 magichan

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

268 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.