協程

協程是計算機程式的一類元件，推廣了協作式多工的子例程，允許執行被掛起與被恢復。相對子例程而言，協程更為一般和靈活，但在實踐中使用沒有子例程那樣廣泛。協程更適合於用來實現彼此熟悉的程式元件，如協作式多工、異常處理、事件迴圈、迭代器、無限列表和管道。根據高德納的說法，馬爾文·康威於1958年發明了術語“coroutine”並用於構建彙編程式 ，關於協程的最初解說在1963年發表。同子例程的比較.協程可以透過codice_1來呼叫其它協程，接下來的每次協程被呼叫時，從協程上次codice_1返回的位置接著執行，透過codice_1方式轉移執行權的協程之間不是呼叫者與被呼叫者的關係，而是彼此對稱、平等的。由於協程不如子例程那樣被普遍所知，下面對它們作簡要比較：子例程可以看作是特定狀況的協程，任何子例程都可轉寫為不呼叫codice_1的協程。示例.這裡是一個簡單的例子證明協程的實用性。假設這樣一種生產者－消費者的關係，一個協程生產產品並將它們加入佇列，另一個協程從佇列中取出產品並消費它們。偽碼錶示如下：var q := new 佇列coroutine 生產者loopwhile q 不滿載建立某些新產品向 q 增加這些產品yield 消費者coroutine 消費者loopwhile q 不空載從 q 移除某些產品使用這些產品yield 生產者佇列用來存放產品的空間有限，同時制約生產者和消費者：為了提高效率，生產者協程要在一次執行中儘量向佇列多增加產品，然後再放棄控制使得消費者協程開始執行；同樣消費者協程也要在一次執行中儘量從佇列多取出產品，從而倒出更多的存放產品空間，然後再放棄控制使得生產者協程開始執行。儘管這個例子常用來介紹多執行緒，實際上簡單明瞭的使用協程的codice_1即可實現這種協作關係。同線程的比較.協程非常類似於執行緒。但是協程是協作式多工的，而典型的執行緒是核心級搶佔式多工的。這意味著協程提供併發性而非並行性。協程超過執行緒的好處是它們可以用於硬性實時的語境。使用者級執行緒是協作式多工的輕量級執行緒，本質上描述了同協程一樣的概念。其區別，如果一定要說有的話，是協程是語言層級的構造，可看作一種形式的控制流，而執行緒是系統層級的構造。生成器.生成器，也叫作“半協程”，是協程的子集。儘管二者都可以codice_1多次，掛起自身的執行，並允許在多個入口點重新進入，但它們特別差異在於，協程有能力控制在它讓位之後哪個協程立即接續它來執行，而生成器不能，它只能把控制權轉交給呼叫生成器的呼叫者。在生成器中的codice_1語句不指定要跳轉到的協程，而是向父例程傳遞返回值。儘管如此，仍可以在生成器設施之上實現協程，這需要透過頂層的分派器所標定的另一個生成器：var q := new 佇列generator 生產者loopwhile q 不滿載建立某些新產品向 q 增加這些產品yield 消費者generator 消費者loopwhile q 不空載從 q 移除某些產品使用這些產品yield 生產者subroutine 分派器var d := new 字典(生成器 → 迭代器)d[生產者] := start 生產者d[消費者] := start 消費者var 當前 := 生產者loopcall 當前當前 := next d[當前]call 分派器在不同作者和語言之間，術語“生成器”和“迭代器”的用法有著微妙的差異。有人說所有生成器都是迭代器，生成器看起來像函式而表現得像迭代器。在Python中，生成器是迭代器構造子：它是返回迭代器的函式。同尾呼叫互遞迴的比較.使用協程用於狀態機或併發執行類似於使用經由尾呼叫的互遞迴，在二者情況下控制權都變更給一組例程中的另一個不同例程。但是，協程更靈活並且一般而言更有效率。因為協程是codice_1而非codice_12返回，接著恢復執行而非在起點重新開始，它們有能力保持狀態，包括變數，而在協程之間傳遞控制權使用現存上下文並可簡單地透過跳轉來實現。協程之常見用例.協程有助於實現：支援協程的程式語言.協程起源於一種組合語言方法，但有一些高階程式語言支援它。早期的例子包括Simula、Smalltalk和Modula-2。更新近的例子是Ruby、Lua、Julia和Go。由於續體可被用來實現協程，支援續體的程式語言也非常容易就支援協程。實現.到2003年，很多最流行的程式語言，包括C語言和它的後繼者，都未在語言內或其標準庫中直接支援協程。。C++的Boost.Context庫是個例外，它是Boost C++ Libraries的一部分，它在POSIX、Mac OS X和Windows上支援多種CPU架構的上下文切換。可以在Boost.Context之上建造協程。在協程是某種機制的最自然的實現方式，卻不能獲得可用協程的情況下，典型的解決方法是使用閉包，它是具有狀態變數的子例程，基於狀態變數來在呼叫之間維持內部狀態，並轉移控制權至正確地點。基於這些狀態變數的值，在程式碼中的條件語句導致在後續呼叫時有著不同程式碼路徑的執行。另一種典型的解決方法實現一個顯式狀態機，採用某種形式的龐大而複雜的或goto語句特別是“計算goto”。這種實現被認為難於理解和維護，更是想要有協程支援的動機。在當今的主流程式設計環境裡，協程的合適的替代者是執行緒和適用範圍較小的纖程。執行緒提供了用來管理“同時”執行的程式碼段實時協作互動的功能，在支援C語言的環境中，執行緒是廣泛有效的，POSIX.1c規定了被稱為pthreads的一個標準執行緒API，它在類Unix系統中被普遍實現。執行緒被很好地實現、文件化和支援，很多程式設計師對其也比較熟悉。但是，執行緒包括了許多強大和複雜的功能用以解決大量困難的問題，這導致了困難的學習曲線，當任務僅需要協程就可完成時，使用執行緒似乎就是用力過猛了。GNU Pth可被視為類Unix系統上使用者級執行緒的代表。C語言實現.C標準庫裡有“非區域性跳轉”函式setjmp/longjmp，它們分別儲存和恢復：棧指標、程式計數器、被呼叫者儲存的暫存器和ABI要求的任何其他內部狀態。在C99標準中，跳轉到已經用codice_12或codice_16終止的函式是未定義的，但是大多數codice_16實現在跳轉時不專門銷燬呼叫棧中的區域性變數，在被後續的函式呼叫等覆寫之前跳轉回來恢復時仍是原樣，這允許在實現協程時謹慎的用到它們。POSIX.1-2001/SUSv3進一步提供了操縱的強力設施：makecontext、、getcontext和swapcontext，可方便地用來實現協程，但是由於codice_18的引數定義利用了具有空圓括號的函式宣告，不符合C99標準要求，這些函式在POSIX.1-2004中被廢棄，並在POSIX.1-2008/SUSv4中被刪除。POSIX.1-2001/SUSv3定義了codice_19，可用來在不能獲得codice_18的情況下稍微迂迴的實現協程。極簡實現不採用有關的標準API函式進行上下文交換，而是寫一小塊內聯彙編只對換棧指標和程式計數器故而速度明顯的要更快。由於缺乏直接的語言支援，很多作者寫了自己的含藏上述技術細節的協程庫，以Russ Cox的libtask協程庫為代表，其目標是讓人“寫事件驅動程式而沒有麻煩的事件”，它可用各種類Unix系統上。知名的實現還有：libpcl、lthread、libconcurrency、libcoro、ribs2、libdill、libaco、libco等等。此外人們做了只用C語言的子例程和宏實現協程的大量嘗試，並取得了不同程度的成功。受到了達夫裝置的啟發，Simon Tatham寫出了很好的協程示範，它利用了“穿透”特性和ANSI C提供的特殊宏名字codice_21，它也是Protothreads和類似實現的基礎。在這種不為每個協程維護獨立的棧幀的實現方式下，區域性變數在經過從函式codice_1之後是不儲存的，控制權只能從頂層例程codice_1。Python實現.Python 2.5基於增強的生成器實現對類似協程功能的更好支援。Python 3.3透過支援委託給子生成器增進了這個能力。Python 3.4介入了綜合性的非同步I/O框架標準化，在其中擴充套件了利用子生成器委託的協程，這個擴充套件在Python 3.8中被棄用。Python 3.5透過async/await語法介入了對協程的顯式支援。從Python 3.7開始async/await成為保留關鍵字。例如：import asyncioimport randomasync def produce(queue, n):for item in range(n):# 生產一個專案，使用sleep模擬I/O操作print('producing item {} -&gt;'.format(item))await asyncio.sleep(random.random())# 將專案放入佇列await queue.put(item)# 指示生產完畢await queue.put(None)async def consume(queue):while True:# 等待來自生產者的專案item = await queue.get()if item is None:break# 消費這個專案，使用sleep模擬I/O操作print('consuming item {} &lt;-'.format(item))await asyncio.sleep(random.random())async def main():queue = asyncio.Queue()task1 = asyncio.create_task(produce(queue, 10))task2 = asyncio.create_task(consume(queue))await task1await task2asyncio.run(main())實現協程的第三方庫：Scheme實現.Scheme提供了對續體的完全支援，實現協程是很輕易的，只需維護一個續體的佇列。在續體條目中有使用續體將協程實現為獨立執行緒的一個用例。Smalltalk實現.在大多數Smalltalk環境中，執行堆疊是頭等公民，實現協程不需要額外的庫或VM支援。Tcl實現.從 Tcl 8.6 開始，Tcl 核心內建協程支援，成為了繼事件迴圈、執行緒後的另一種內建的強大功能。