協程

協程是計算機程式的一類元件，推廣了協作式多工的子例程，允許執行被掛起與被恢復。相對子例程而言，協程更為一般和靈活，但在實踐中使用沒有子例程那樣廣泛。協程更適合於用來實現彼此熟悉的程式元件，如協作式多工、異常處理、事件迴圈、迭代器、無限列表和管道。

根據高德納的說法，馬爾文・康威於 1958 年發明了術語 “coroutine” 並用於構建彙編程式 ，關於協程的最初解說在 1963 年發表。

同子例程的比較

協程可以透過 yield（取其 “退讓” 之義而非 “產生”）來呼叫其它協程，接下來的每次協程被呼叫時，從協程上次 yield 返回的位置接著執行，透過 yield 方式轉移執行權的協程之間不是呼叫者與被呼叫者的關係，而是彼此對稱、平等的。由於協程不如子例程那樣被普遍所知，下面對它們作簡要比較：

子例程可以呼叫其他子例程，呼叫者等待被呼叫者結束後繼續執行，故而子例程的生命期遵循後進先出，即最後一個被呼叫的子例程最先結束返回。協程的生命期完全由對它們的使用需要來決定。
子例程的起始處是惟一的入口點，每當子例程被呼叫時，執行都從被呼叫子例程的起始處開始。協程可以有多個入口點，協程的起始處是第一個入口點，每個 yield 返回出口點都是再次被呼叫執行時的入口點。
子例程只在結束時一次性的返回全部結果值。協程可以在 yield 時不呼叫其他協程，而是每次返回一部份的結果值，這種協程常稱為生成器或迭代器。
現代的指令集架構通常提供對呼叫棧的指令支援，便於實現可遞迴呼叫的子例程。在以 Scheme 為代表的提供續體的語言環境下，恰好可用此控制狀態抽象表示來實現協程。

子例程可以看作是特定狀況的協程，任何子例程都可轉寫為不呼叫 yield 的協程。

偽碼示意

這裡是一個簡單的例子證明協程的實用性。假設這樣一種生產者－消費者的關係，一個協程生產產品並將它們加入佇列，另一個協程從佇列中取出產品並消費它們。偽碼錶示如下：

var q := new 佇列

coroutine 生產者

loop

while q 不滿載

建立某些新產品

向 q 增加這些產品

yield 消費者

coroutine 消費者

loop

while q 不空載

從 q 移除某些產品

使用這些產品

yield 生產者

佇列用來存放產品的空間有限，同時制約生產者和消費者：為了提高效率，生產者協程要在一次執行中儘量向佇列多增加產品，然後再放棄控制使得消費者協程開始執行；同樣消費者協程也要在一次執行中儘量從佇列多取出產品，從而倒出更多的存放產品空間，然後再放棄控制使得生產者協程開始執行。儘管這個例子常用來介紹多執行緒，實際上簡單明瞭的使用協程的 yield 即可實現這種協作關係。

同線程的比較

協程非常類似於執行緒。但是協程是協作式多工的，而典型的執行緒是核心級搶佔式多工的。這意味著協程提供併發性而非並行性。協程超過執行緒的好處是它們可以用於硬性即時的語境（在協程之間的切換不需要涉及任何系統呼叫或任何阻塞呼叫），這裡不需要用來守衛關鍵區段的同步性原語（primitive）比如互斥鎖、訊號量等，並且不需要來自作業系統的支援。有可能以一種對呼叫程式碼透明的方式，使用搶佔式排程的執行緒實現協程，但是會失去某些利益（特別是對硬性即時操作的適合性和相對廉價的相互之間切換）。

使用者級執行緒是協作式多工的輕量級執行緒，本質上描述了同協程一樣的概念。其區別，如果一定要說有的話，是協程是語言層級的構造，可看作一種形式的控制流程，而執行緒是系統層級的構造。

生成器

生成器，也叫作 “半協程”，是協程的子集。儘管二者都可以 yield 多次，暫停（suspend）自身的執行，並允許在多個入口點重新進入，但它們特別差異在於，協程有能力控制在它讓位之後哪個協程立即接續它來執行，而生成器不能，它只能把控制權轉交給呼叫生成器的呼叫者。在生成器中的 yield 語句不指定要跳轉到的協程，而是向父例程傳遞返回值。

儘管如此，仍可以在生成器設施之上實現協程，這需要透過頂層的分派器（dispatcher）例程（實質上是）的援助，它顯式的把控制權傳遞給由生成器傳回的記號 / 令牌（token）所標定的另一個生成器：

var q := new 佇列

generator 生產者

loop

while q 不滿載

建立某些新產品

向 q 增加這些產品

yield 消費者

generator 消費者

loop

while q 不空載

從 q 移除某些產品

使用這些產品

yield 生產者

subroutine 分派器

var d := new 字典 (生成器 → 迭代器)

d 生產者 := start 生產者

d 消費者 := start 消費者

var 當前 := 生產者

loop

call 當前

當前 := next d 當前

call 分派器

在不同作者和語言之間，術語 “生成器” 和 “迭代器” 的用法有著微妙的差異。有人說所有生成器都是迭代器，生成器看起來像函式而表現得像迭代器。在 Python 中，生成器是迭代器構造子：它是返回迭代器的函式。

同尾呼叫互遞迴的比較

使用協程用於狀態機或併發執行類似於使用經由尾呼叫的互遞迴，在二者情況下控制權都變更給一組例程中的另一個不同例程。但是，協程更靈活並且一般而言更有效率。因為協程是 yield 而非 return 返回，接著恢復執行而非在起點重新開始，它們有能力保持狀態，包括變數（同於閉包）和執行點二者，並且 yield 不限於位於尾部位置；互遞迴子例程必須要麼使用共享變數，要麼把狀態作為引數傳遞。進一步的說，每一次子例程的互遞迴呼叫都需要一個新的棧幀（除非實現了尾呼叫消去），而在協程之間傳遞控制權使用現存上下文並可簡單地透過跳轉來實現。

協程之常見用例

協程有助於實現：

狀態機：在單一子例程裡實現狀態機，這裡狀態由該過程當前的出口 / 入口點確定；這可以產生可讀性更高的程式碼。
演員模型：併發的演員模型，例如計算機遊戲。每個演員有自己的過程（這又在邏輯上分離了程式碼），但他們自願地向順序執行各演員過程的中央排程器交出控制（這是合作式多工的一種形式）。
生成器：可用於串流，特別是輸入 / 輸出串流，和對資料結構的通用遍歷。
通訊順序程序：這裡每個子程序都是協程。通道輸入 / 輸出和阻塞操作會 yield 協程，並由排程器在有完成事件時對其解除阻塞（unblock）。可作為替代的方式是，每個子程序可以是在資料管道中位於其後的子程序的父程序（或是位於其前者之父，這種情況下此模式可以表達為巢狀的生成器）。

支援協程的程式語言

協程起源於一種組合語言方法，但有一些高階程式語言支援它。早期的例子包括 Simula、Smalltalk 和 Modula-2。更新近的例子是 Ruby、Lua、Julia 和 Go。

Aikido
BCPL
Pascal（Borland Turbo Pascal 7.0 帶有 uThreads 模組）
BETA
C++（自從 C++20）
C#（自從 2.0）
ChucK
CLU
D
Dynamic C
Erlang
F#
Factor
GDScript（Godot 的指令碼語言）
Go
Haskell
高階組合語言
Icon
Io
JavaScript（自從 1.7，標準化於 ECMAScript 6，ECMAScript 2017 還包括 async/await 支援）
Julia
Kotlin（自從 1.1）
Limbo
Lua
Lucid
Modula-2
Nemerle
Perl 5（使用 Coro 模組）
PHP（帶有 hiphop-php，原生支援自從 PHP 5.5）
Picolisp
Prolog
Python（自從 2.5，帶有改進支援自從 3.3，帶有顯式語法自從 3.5）
Raku
Ruby
Scheme
Self
Simula 67
Smalltalk
Stackless Python
SuperCollider
Tcl（自從 8.6）

由於續體可被用來實現協程，支援續體的程式語言也非常容易就支援協程。

實現

直到 2003 年，很多最流行的程式語言，包括 C 語言和它的後繼者，都未在語言內或其標準庫中直接支援協程。這在很大程度上是受基於堆疊的子例程實現的限制。C++ 的 Boost.Context 庫是個例外，它是 Boost C++ 庫的一部分，它在 POSIX、Mac OS X 和 Windows 上支援多種 CPU 架構的上下文切換。可以在 Boost.Context 之上建造協程。

在協程是某種機制的最自然的實現方式，卻不能獲得可用協程的情況下，典型的解決方法是使用閉包，它是具有狀態變數（靜態變數，常為布林標誌值）的子例程，基於狀態變數來在呼叫之間維持內部狀態，並轉移控制權至正確地點。基於這些狀態變數的值，在程式碼中的條件語句導致在後續呼叫時有著不同程式碼路徑的執行。另一種典型的解決方法實現一個顯式狀態機，採用某種形式的龐大而複雜的或 goto 語句特別是 “計算 goto”。這種實現被認為難於理解和維護，更是想要有協程支援的動機。

在當今的主流程式設計環境裡，協程的合適的替代者是執行緒和適用範圍較小的纖程。執行緒提供了用來管理 “同時” 執行的程式碼段即時協作互動的功能，在支援 C 語言的環境中，執行緒是廣泛有效的，POSIX.1c（IEEE Std 1003.1c-1995）規定了被稱為 pthreads 的一個標準執行緒 API，它在類 Unix 系統中被普遍實現。執行緒被很好地實現、文件化和支援，很多程式設計師對其也比較熟悉。但是，執行緒包括了許多強大和複雜的功能用以解決大量困難的問題，這導致了困難的學習曲線，當任務僅需要協程就可完成時，使用執行緒似乎就是用力過猛了。GNU Pth 可被視為類 Unix 系統上使用者級執行緒的代表。

8.1 C

C 標準庫裡有 “非區域性跳轉” 函式 setjmp/longjmp，它們分別儲存和恢復：棧指標、程式計數器、被呼叫者儲存的暫存器和 ABI 要求的任何其他內部狀態。在 C99 標準中，跳轉到已經用 return 或 longjmp 終止的函式是未定義的，但是大多數 longjmp 實現在跳轉時不專門銷燬呼叫棧中的區域性變數，在被後續的函式呼叫等覆寫之前跳轉回來恢復時仍是原樣，這允許在實現協程時謹慎的用到它們。

POSIX.1-2001/SUSv3 進一步提供了操縱的強力設施：makecontext、、getcontext 和 swapcontext，可方便地用來實現協程，但是由於 makecontext 的引數定義利用了具有空圓括號的函式宣告，不符合 C99 標準要求，這些函式在 POSIX.1-2004 中被廢棄，並在 POSIX.1-2008/SUSv4 中被刪除。POSIX.1-2001/SUSv3 定義了 sigaltstack，可用來在不能獲得 makecontext 的情況下稍微迂迴的實現協程。極簡實現不採用有關的標準 API 函式進行上下文交換，而是寫一小塊內聯彙編只對換棧指標和程式計數器故而速度明顯的要更快。

由於缺乏直接的語言支援，很多作者寫了自己的含藏上述技術細節的協程庫，以 Russ Cox 的 libtask 協程庫為代表，其目標是讓人 “寫事件驅動程式而沒有麻煩的事件”，它可用各種類 Unix 系統上。知名的實現還有：libpcl、lthread、libconcurrency、libcoro、libdill、libaco、libco 等等。

此外人們做了只用 C 語言的子例程和宏實現協程的大量嘗試，並取得了不同程度的成功。受到了達夫裝置的啟發，Simon Tatham 寫出了很好的協程示範，它利用了 “穿透”（fallthrough）特性，和 ANSI C 提供的包含了原始碼的當前行號的特殊宏名字LINE，它也是 Protothreads 和類似實現的基礎。在這種不為每個協程維護獨立的棧幀的實現方式下，區域性變數在經過從函式 yield 之後是不儲存的，控制權只能從頂層例程 yield。

8.2 C++

C++20 介入了作為無棧函式的標準化的協程，它可以在執行中間暫停並在後來某點上恢復。協程的暫停狀態儲存在堆之上。這個標準的實現正在進行中，目前 G++ 和 MSVC 在新近版本中完全支援了標準協程。

8.3 C#

C# 2.0 透過迭代器模式增加了半協程（生成器）功能並增加了 yield 關鍵字，C# 5.0 包括了 await 語法支援。

8.4 Go

Go 擁有內建的 “goroutine” 概念，它是輕量級的、無關於程序並由 Go 執行時系統來管理。可以使用 go 關鍵字來啟動一個新的 goroutine。每個 goroutine 擁有可變大小的能在需要時擴充套件的棧。goroutine 一般使用 Go 的內建通道來通訊。

8.5 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 asyncio

import random

async def produce (queue, n):

for item in range (n):

生產一個專案，使用 sleep 模擬 I/O 操作

print (f'producing item {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 (f'consuming item {item}

實現協程的第三方庫：

Eventlet
Greenlet
gevent
Stackless Python

8.6 Perl

Coro 是 Perl5 中的一種協程實現，它使用 C 作為底層，所以具有良好的執行效能，而且可以配合 AnyEvent 共同使用，極大的彌補了 Perl 線上程上劣勢。

8.7 Scheme

Scheme 提供了對頭等續體的完全支援，實現協程是很輕易的，在續體條目中有使用頭等續體將協程實現為執行緒的示例。

8.8 Smalltalk

在大多數 Smalltalk 環境中，執行堆疊是頭等公民，實現協程不需要額外的庫或 VM 支援。

8.9 Tcl

從 Tcl 8.6 開始，Tcl 核心內建協程支援，成為了繼事件迴圈、執行緒後的另一種內建的強大功能。