Loading AI tools
来自维基百科,自由的百科全书
共常式(英語:coroutine)是電腦程式的一類組件,推廣了協同運作式多工的次常式,允許執行被掛起與被恢復。相對次常式而言,共常式更為一般和靈活,但在實踐中使用沒有次常式那樣廣泛。共常式更適合於用來實現彼此熟悉的程式組件,如協同運作式多工、例外處理、事件迴圈、迭代器、無限列表和管道。
根據高德納的說法,馬爾文·康威於1958年發明了術語「coroutine」並用於構建組譯程式[1] ,關於共常式的最初解說在1963年發表[2]。
共常式可以通過yield
(取其「退讓」之義而非「產生」)來呼叫其它共常式,接下來的每次共常式被呼叫時,從共常式上次yield
返回的位置接著執行,通過yield
方式轉移執行權的共常式之間不是呼叫者與被呼叫者的關係,而是彼此對稱、平等的。由於共常式不如次常式那樣被普遍所知,下面對它們作簡要比較:
yield
返回出口點都是再次被呼叫執行時的入口點。yield
時不呼叫其他共常式,而是每次返回一部份的結果值,這種共常式常稱為生成器或迭代器。這裡是一個簡單的例子證明共常式的實用性。假設這樣一種生產者-消費者的關係,一個共常式生產產品並將它們加入佇列,另一個共常式從佇列中取出產品並消費它們。偽碼表示如下:
var q := new 队列 coroutine 生产者 loop while q 不满载 建立某些新产品 向 q 增加这些产品 yield 消费者 coroutine 消费者 loop while q 不空载 从 q 移除某些产品 使用这些产品 yield 生产者
佇列用來存放產品的空間有限,同時制約生產者和消費者:為了提高效率,生產者共常式要在一次執行中儘量向佇列多增加產品,然後再放棄控制使得消費者共常式開始執行;同樣消費者共常式也要在一次執行中儘量從佇列多取出產品,從而倒出更多的存放產品空間,然後再放棄控制使得生產者共常式開始執行。儘管這個例子常用來介紹多執行緒,實際上簡單明了的使用共常式的yield
即可實現這種協同運作關係。
共常式非常類似於執行緒。但是共常式是協同運作式多工的,而典型的執行緒是核心級搶占式多工的。這意味著共常式提供並行性而非並列性。共常式超過執行緒的好處是它們可以用於硬性即時的語境(在共常式之間的切換不需要涉及任何系統呼叫或任何阻塞呼叫),這裡不需要用來守衛關鍵區段的同步性原語(primitive)比如互斥鎖、號誌等,並且不需要來自作業系統的支援。有可能以一種對呼叫代碼透明的方式,使用搶占式排程的執行緒實現共常式,但是會失去某些利益(特別是對硬性即時操作的適合性和相對廉價的相互之間切換)。
使用者級執行緒是協同運作式多工的輕量級執行緒,本質上描述了同共常式一樣的概念。其區別,如果一定要說有的話,是共常式是語言層級的構造,可看作一種形式的控制流程,而執行緒是系統層級的構造。
生成器,也叫作「半共常式」[6],是共常式的子集。儘管二者都可以yield
多次,暫停(suspend
)自身的執行,並允許在多個入口點重新進入,但它們特別差異在於,共常式有能力控制在它讓位之後哪個共常式立即接續它來執行,而生成器不能,它只能把控制權轉交給呼叫生成器的呼叫者[7]。在生成器中的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 分派器
在不同作者和語言之間,術語「生成器」和「迭代器」的用法有著微妙的差異[8]。有人說所有生成器都是迭代器[9],生成器看起來像函式而表現得像迭代器。在Python中,生成器是迭代器構造子:它是返回迭代器的函式。
使用共常式用於狀態機或並行執行類似於使用經由尾呼叫的互遞迴,在二者情況下控制權都變更給一組常式中的另一個不同常式。但是,共常式更靈活並且一般而言更有效率。因為共常式是yield
而非return
返回,接著恢復執行而非在起點重新開始,它們有能力保持狀態,包括變數(同於閉包)和執行點二者,並且yield
不限於位於尾部位置;互遞迴次常式必須要麼使用共享變數,要麼把狀態作為參數傳遞。進一步的說,每一次次常式的互遞迴呼叫都需要一個新的堆疊框(除非實現了尾呼叫消去),而在共常式之間傳遞控制權使用現存上下文並可簡單地通過跳轉來實現。
共常式有助於實現:
yield
共常式,並由排程器在有完成事件時對其解除阻塞(unblock)。可作為替代的方式是,每個子行程可以是在資料管道中位於其後的子行程的父行程(或是位於其前者之父,這種情況下此模式可以表達為巢狀的生成器)。共常式起源於一種組合語言方法,但有一些高階程式語言支援它。早期的例子包括Simula[6]、Smalltalk和Modula-2。更新近的例子是Ruby、Lua、Julia和Go。
由於續體可被用來實現共常式,支援續體的程式語言也非常容易就支援共常式。
直到2003年,很多最流行的程式語言,包括C語言和它的後繼者,都未在語言內或其標準庫中直接支援共常式。這在很大程度上是受基於堆疊的次常式實現的限制。C++的Boost.Context[24]庫是個例外,它是Boost C++庫的一部分,它在POSIX、Mac OS X和Windows上支援多種CPU架構的上下文交換。可以在Boost.Context之上建造共常式。
在共常式是某種機制的最自然的實現方式,卻不能獲得可用共常式的情況下,典型的解決方法是使用閉包,它是具有狀態變數(靜態變數,常為布林標誌值)的次常式,基於狀態變數來在呼叫之間維持內部狀態,並轉移控制權至正確地點。基於這些狀態變數的值,在代碼中的條件語句導致在後續呼叫時有著不同代碼路徑的執行。另一種典型的解決方法實現一個顯式狀態機,採用某種形式的龐大而複雜的switch語句或goto語句特別是「計算goto」。這種實現被認為難於理解和維護,更是想要有共常式支援的動機。
在當今的主流編程環境裡,共常式的合適的替代者是執行緒和適用範圍較小的纖程。執行緒提供了用來管理「同時」執行的代碼段即時協同運作互動的功能,在支援C語言的環境中,執行緒是廣泛有效的,POSIX.1c(IEEE Std 1003.1c-1995)規定了被稱為pthreads的一個標準執行緒API,它在類Unix系統中被普遍實現。執行緒被很好地實現、文件化和支援,很多程式設計師對其也比較熟悉。但是,執行緒包括了許多強大和複雜的功能用以解決大量困難的問題,這導致了困難的學習曲線,當任務僅需要共常式就可完成時,使用執行緒似乎就是用力過猛了。GNU Pth可被視為類Unix系統上使用者級執行緒的代表。
C標準庫里有「非局部跳轉」函式setjmp/longjmp,它們分別儲存和恢復:棧指標、程式計數器、被呼叫者儲存的暫存器和ABI要求的任何其他內部狀態。在C99標準中,跳轉到已經用return
或longjmp
終止的函式是未定義的[25],但是大多數longjmp
實現在跳轉時不專門銷毀呼叫棧中的局部變數,在被後續的函式呼叫等覆寫之前跳轉回來恢復時仍是原樣,這允許在實現共常式時謹慎的用到它們。
POSIX.1-2001/SUSv3進一步提供了操縱上下文的強力設施:makecontext、setcontext、getcontext和swapcontext,可方便地用來實現共常式,但是由於makecontext
的參數定義利用了具有空圓括號的函式聲明,不符合C99標準要求,這些函式在POSIX.1-2004中被廢棄,並在POSIX.1-2008/SUSv4中被刪除[26]。POSIX.1-2001/SUSv3定義了sigaltstack
,可用來在不能獲得makecontext
的情況下稍微迂迴的實現共常式[27]。極簡實現不採用有關的標準API函式進行上下文交換,而是寫一小塊內聯組譯只對換棧指標和程式計數器故而速度明顯的要更快。
由於缺乏直接的語言支援,很多作者寫了自己的含藏上述技術細節的共常式庫,以Russ Cox的libtask共常式庫為代表[28],其目標是讓人「寫事件驅動程式而沒有麻煩的事件」,它可用各種類Unix系統上。知名的實現還有:libpcl[29]、lthread[30]、libconcurrency[31]、libcoro[32]、libdill[33]、libaco[34]、libco[35]等等。
此外人們做了只用C語言的次常式和宏實現共常式的大量嘗試,並取得了不同程度的成功。受到了達夫裝置的啟發,Simon Tatham寫出了很好的共常式示範[36],它利用了swtich語句「穿透」(fallthrough)特性[37],和ANSI C提供的包含了原始碼的當前行號的特殊宏名字__LINE__
,它也是Protothreads和類似實現的基礎[38]。在這種不為每個共常式維護獨立的堆疊框的實現方式下,局部變數在經過從函式yield
之後是不儲存的,控制權只能從頂層常式yield
[39]。
C++20介入了作為無棧函式的標準化的共常式,它可以在執行中間暫停並在後來某點上恢復。共常式的暫停狀態儲存在堆之上[40]。這個標準的實現正在進行中,目前G++和MSVC在新近版本中完全支援了標準共常式[41]。
C# 2.0通過迭代器模式增加了半共常式(生成器)功能並增加了yield
關鍵字[42][43],C# 5.0包括了await語法支援。
Go擁有內建的「goroutine」概念,它是輕量級的、無關於行程並由Go執行時系統來管理。可以使用go
關鍵字來啟動一個新的goroutine。每個goroutine擁有可變大小的能在需要時擴充的棧。goroutine一般使用Go的內建通道來通訊[44][45][46][47]。
Python 2.5基於增強的生成器實現對類似共常式功能的更好支援[19]。Python 3.3通過支援委託給子生成器增進了這個能力[20]。Python 3.4介入了綜合性的非同步I/O框架標準化,在其中擴充了利用子生成器委託的共常式[48],這個擴充在Python 3.8中被棄用[49]。Python 3.5通過async/await語法介入了對共常式的顯式支援[50]。從Python 3.7開始async/await成為保留關鍵字[51]。例如:
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} <-')
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 task1
await task2
asyncio.run(main())
實現共常式的第三方庫:
Coro是Perl5中的一種共常式實現[56],它使用C作為底層,所以具有良好的執行效能,而且可以配合AnyEvent共同使用,極大的彌補了Perl線上程上劣勢。
Scheme提供了對頭等續體的完全支援,實現共常式是很輕易的,在續體條目中有使用頭等續體將共常式實現為執行緒的範例[57]。
在大多數Smalltalk環境中,執行堆疊是頭等公民,實現共常式不需要額外的庫或VM支援。
從 Tcl 8.6 開始,Tcl 核心內建共常式支援,成為了繼事件迴圈、執行緒後的另一種內建的強大功能。
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.