如果 Python 書籍有一定的指導作用,那么(協程就是)文檔最匱乏、最鮮為人知的 Python 特性,因此表面上看是最無用的特性。
——David Beazley
Python 圖書作者
字典為動詞“to yield”給出了兩個釋義:產出和讓步。對於 Python 生成器中的 yield 來說,這兩個含義都成立。yield item 這行代碼會產出一個值,提供給 next(...) 的調用方;此外,還會作出讓步,暫停執行生成器,讓調用方繼續工作,直到需要使用另一個值時再調用next()。調用方會從生成器中拉取值。
從句法上看,協程與生成器類似,都是定義體中包含 yield 關鍵字的函數。可是,在協程中,yield 通常出現在表達式的右邊(例如,datum = yield),可以產出值,也可以不產出——如果 yield關鍵字后面沒有表達式,那么生成器產出 None。協程可能會從調用方接收數據,不過調用方把數據提供給協程使用的是 .send(datum) 方法,而不是 next(...) 函數。通常,調用方會把值推送給協程。
yield 關鍵字甚至還可以不接收或傳出數據。不管數據如何流動,yield 都是一種流程控制工具,使用它可以實現協作式多任務:協程可以把控制器讓步給中心調度程序,從而激活其他的協程。
從根本上把 yield 視作控制流程的方式,這樣就好理解協程了。
生成器如何進化成協程
協程的底層架構在“PEP 342—Coroutines via EnhancedGenerators”(https://www.python.org/dev/peps/pep-0342/)中定義,並在Python 2.5(2006 年)實現了。自此之后,yield 關鍵字可以在表達式中使用,而且生成器 API 中增加了 .send(value) 方法。生成器的調用方可以使用 .send(...) 方法發送數據,發送的數據會成為生成器函數中 yield 表達式的值。因此,生成器可以作為協程使用。協程是指一個過程,這個過程與調用方協作,產出由調用方提供的值。
除了 .send(...) 方法,PEP 342 還添加了 .throw(...) 和 .close()方法:前者的作用是讓調用方拋出異常,在生成器中處理;后者的作用是終止生成器。
用作協程的生成器的基本行為
舉個 🌰 演示協程的用法
1 def simple_coroutine(): # 攜程使用生成器函數定義:定義題中有yield關鍵字 2 print('-> coroutine started') # 如果攜程只從客戶那里接受數據,那么產出的值是None,這個值是隱式的,因為yield關鍵字右邊沒有表達式 3 x = yield 4 print('-> coroutine received:', x) 5 6 my_coro = simple_coroutine() 7 print(my_coro) # 與創建生成器的方式一樣,調用函數得到生成器對象 8 next(my_coro) # 首先要調用next(..)函數,因為生成器還沒有啟動,沒在yield語句初暫停,所以一開始無法發送數據 9 10 my_coro.send(10) # 調用這個方法后,攜程定義中的yield表但是會出現10,直到下一個yield出現或者終止
以上代碼執行的結果為:
<generator object simple_coroutine at 0x102a463b8> -> coroutine started -> coroutine received: 10 Traceback (most recent call last): ........... my_coro.send(10) # 調用這個方法后,攜程定義中的yield表但是會出現10,直到下一個yield出現或者終止 StopIteration
協程可以身處四個狀態中的一個。當前狀態可以使用inspect.getgeneratorstate(...) 函數確定,該函數會返回下述字符串中的一個。
'GEN_CREATED'
等待開始執行。
'GEN_RUNNING'
解釋器正在執行。
'GEN_SUSPENDED'
在 yield 表達式處暫停。
'GEN_CLOSED'
執行結束。
因為 send 方法的參數會成為暫停的 yield 表達式的值,所以,僅當協程處於暫停狀態時才能調用 send 方法,例如 my_coro.send(10)。不過,如果協程還沒激活(即,狀態是 'GEN_CREATED'),情況就不同了。因此,始終要調用 next(my_coro) 激活協程——也可以調用my_coro.send(None),效果一樣。
如果創建協程對象后立即把 None 之外的值發給它,會出現下述錯誤:
>>> my_coro = simple_coroutine() >>> my_coro.send(1729) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't send non-None value to a just-started generator
注意錯誤消息,它表述得相當清楚。最先調用 next(my_coro) 函數這一步通常稱為“預激”(prime)協程(即,讓協程向前執行到第一個 yield 表達式,准備好作為活躍的協程使用)。
下面舉個產出多個值的例子,以便更好地理解協程的行為,🌰 如下
>>> def simple_coro2(a): ... print('-> Started: a =', a) ... b = yield a ... print('-> Received: b =', b) ... c = yield a + b ... print('-> Received: c =', c) ... >>> my_coro2 = simple_coro2(14) >>> from inspect import getgeneratorstate >>> getgeneratorstate(my_coro2) # 指明狀態,處於GEN_CREATED狀態,也就是等着next一下 'GEN_CREATED' >>> next(my_coro2) # 向前執行協程到第一個yield表達式,打印-> Started 這個信息以后,然后產出的a的值,並且停止,等待為b賦值 -> Started: a = 14 14 >>> getgeneratorstate(my_coro2) # 查看協程的狀態,現在處於GEN_SUSPENDED狀態(即協程在yield表達式處暫停) 'GEN_SUSPENDED' >>> my_coro2.send(28) # 把數字28發給暫停的協程,計算yield表達式,得到28,然后綁定給b,產出a + b的值(42),然后協程暫停,等待為c賦值 -> Received: b = 28 42 >>> my_coro2.send(99) # 把數字99發送給暫停的協程,計算yield表達式,得到99,然后把得到的數字綁定給c,然后協程終止。導致生成器拋出StopIteration -> Received: c = 99 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> getgeneratorstate(my_coro2) # 協程的狀態處於GEN_CLOSED狀態 'GEN_CLOSED'
關鍵的一點是,協程在 yield 關鍵字所在的位置暫停執行。前面說過,在賦值語句中,= 右邊的代碼在賦值之前執行。因此,對於 b =yield a 這行代碼來說,等到客戶端代碼再激活協程時才會設定 b 的值。這種行為要花點時間才能習慣,不過一定要理解,這樣才能弄懂異步編程中 yield 的作用。
simple_coro2 協程的執行過程分為 3 個階段,如圖下圖所示。
(1) 調用 next(my_coro2),打印第一個消息,然后執行 yield a,產出數字 14。
(2) 調用 my_coro2.send(28),把 28 賦值給 b,打印第二個消息,然后執行 yield a + b,產出數字 42。
(3) 調用 my_coro2.send(99),把 99 賦值給 c,打印第三個消息,協程終止。
執行 simple_coro2 協程的 3 個階段(注意,各個階段都在yield 表達式中結束,而且下一個階段都從那一行代碼開始,然后再把 yield 表達式的值賦給變量)
示例:使用協程計算移動平均值
使用協程的好處是,total 和 count 聲明為局部變量即可,無需使用實例屬性或閉包在多次調用之間保持上下文。下面的 🌰 是使用averager 協程的 doctest。
coroaverager0.py:定義一個計算移動平均值的協程
1 def averager(): 2 total = 0 3 count = 0 4 average = None 5 while True: # 無限循環一直會不斷的把值發送給這個協程,它就會一直接受,然后生成結果 6 # 僅當調用方在協程上調用.close()方法,或者沒有對協程引用的時候才會終止 7 term = yield average # 這里的yield表達式用於暫停執行協程,把結果發送給調用方,還用於接受調后面發給協程的值 8 total += term 9 count += 1 10 average = total/count
以上代碼執行的結果為:
>>> coro_avg = averager() # 創建協程對象 >>> next(coro_avg) # 調用next函數,預激協程 >>> coro_avg.send(10) # 計算平均值:多次調用send(...)方法,產出當前平均值 10.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(5) 15.0
在上述 doctest 中,調用 next(coro_avg) 函數后,協程會向前執行到 yield 表達式,產出 average 變量的初始值——None,因此不會出現在控制台中。此時,協程在 yield 表達式處暫停,等到調用方發送值。coro_avg.send(10) 那一行發送一個值,激活協程,把發送的值賦給 term,並更新 total、count 和 average 三個變量的值,然后開始 while 循環的下一次迭代,產出 average 變量的值,等待下一次為 term 變量賦值。
預激協程的裝飾器
如果不預激,那么協程沒什么用。調用 my_coro.send(x) 之前,記住一定要調用 next(my_coro)。為了簡化協程的用法,有時會使用一個預激裝飾器。
1 from functools import wraps 2 from inspect import getgeneratorstate 3 4 5 def coroutine(func): 6 @wraps(func) 7 def primer(*args, **kwargs): # 把被裝飾的生成器函數天換成這里的primer函數,調用peimer函數時,返回預激后的生成器 8 gen = func(*args, **kwargs) # 獲取生成器對象 9 next(gen) # 預激活 10 return gen # 返回生成器 11 return primer 12 13 14 @coroutine # 預激活裝飾器 15 def averager(): 16 total = 0 17 count = 0 18 average = None 19 while True: # 無限循環一直會不斷的把值發送給這個協程,它就會一直接受,然后生成結果 20 # 僅當調用方在協程上調用.close()方法,或者沒有對協程引用的時候才會終止 21 term = yield average # 這里的yield表達式用於暫停執行協程,把結果發送給調用方,還用於接受調后面發給協程的值 22 total += term 23 count += 1 24 average = total/count 25 26 coro_avg = averager() # 調用averager()函數創建一個生成器對象,在coroutine裝飾器的primer函數中已預激活 27 print(getgeneratorstate(coro_avg)) # 查看協程的狀態,已經是可以接收值得狀態咯 28 print(coro_avg.send(10)) # 給協程發送數據 29 print(coro_avg.send(30)) 30 print(coro_avg.send(5))
以上代碼的執行結果為:
GEN_SUSPENDED
10.0
20.0
15.0
終止協程和異常處理
協程中未處理的異常會向上冒泡,傳給 next 函數或 send 方法的調用方(即觸發協程的對象)
>>> from coroaverager1 import averager >>> coro_avg = averager() >>> coro_avg.send(40) # 使用@corotine裝飾器裝飾的averager協程,可以立即開始發送值 40.0 >>> coro_avg.send(50) 45.0 >>> coro_avg.send('spam') # 發送的值不是數字,導致協程內部有異常拋出 Traceback (most recent call last): ... TypeError: unsupported operand type(s) for +=: 'float' and 'str' >>> coro_avg.send(60) # 由於異常沒有處理,so...協程會終止。如果試圖重新激活協程,會拋出StopIteration異常 Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
出錯的原因是,發送給協程的 'spam' 值不能加到 total 變量上
暗示了終止協程的一種方式:發送某個哨符值,讓協程退出。內置的 None 和 Ellipsis 等常量經常用作哨符值。Ellipsis 的優點是,數據流中不太常有這個值。我還見過有人把 StopIteration類(類本身,而不是實例,也不拋出)作為哨符值;也就是說,是像這樣使用的:my_coro.send(StopIteration)。
從 Python 2.5 開始,客戶代碼可以在生成器對象上調用兩個方法,顯式地把異常發給協程。
這兩個方法是 throw 和 close:
generator.throw(exc_type[, exc_value[, traceback]])
致使生成器在暫停的 yield 表達式處拋出指定的異常。如果生成器處理了拋出的異常,代碼會向前執行到下一個 yield 表達式,而產出的值會成為調用 generator.throw 方法得到的返回值。如果生成器沒有處理拋出的異常,異常會向上冒泡,傳到調用方的上下文中。
generator.close()
致使生成器在暫停的 yield 表達式處拋出 GeneratorExit 異常。如果生成器沒有處理這個異常,或者拋出了 StopIteration 異常(通常是指運行到結尾),調用方不會報錯。如果收到 GeneratorExit 異常,生成器一定不能產出值,否則解釋器會拋出 RuntimeError 異常。生成器拋出的其他異常會向上冒泡,傳給調用方。
coro_exc_demo.py:學習在協程中處理異常的測試代碼
1 class DemoException(Exception): 2 """為了掩飾定義的異常類型""" 3 4 def demo_exc_handling(): 5 print('-> coroutine startedd') 6 while True: 7 try: 8 x = yield 9 except DemoException: # 特別處理 DemoException 異常 10 print('*** DemoException handled. Continuing...') 11 else: # 沒有異常就接收值 12 print('-> coroutine received: {!r}'.format(x)) 13 14 raise RuntimeError('This line should never run.') # while True會不停止的循環,這一樣會一直執行
激活和關閉 demo_exc_handling,沒有異常
>>> exc_coro = demo_exc_handling() >>> next(exc_coro) -> coroutine started >>> exc_coro.send(11) -> coroutine received: 11 >>> exc_coro.send(22) -> coroutine received: 22 >>> exc_coro.close() >>> from inspect import getgeneratorstate >>> getgeneratorstate(exc_coro) 'GEN_CLOSED'
如果把 DemoException 異常傳入 demo_exc_handling 協程,它會處理,然后繼續運行,如 🌰 所示
>>> exc_coro = demo_exc_handling() >>> next(exc_coro) -> coroutine started >>> exc_coro.send(11) -> coroutine received: 11 >>> exc_coro.throw(DemoException) *** DemoException handled. Continuing... >>> getgeneratorstate(exc_coro) 'GEN_SUSPENDED'
但是,如果傳入協程的異常沒有處理,協程會停止,即狀態變成'GEN_CLOSED'
>>> exc_coro = demo_exc_handling() >>> next(exc_coro) -> coroutine started >>> exc_coro.send(11) -> coroutine received: 11 >>> exc_coro.throw(ZeroDivisionError) Traceback (most recent call last): ... ZeroDivisionError >>> getgeneratorstate(exc_coro) 'GEN_CLOSED'
如果不管協程如何結束都想做些清理工作,要把協程定義體中相關的代碼放入 try/finally 塊中
🌰 coro_finally_demo.py:使用 try/finally 塊在協程終止時執行操作
1 class DemoException(Exception): 2 pass 3 4 def demo_finall(): 5 print('-> coroutine started') 6 try: 7 while True: 8 try: 9 x = yield 10 except DemoException: 11 print('*** DemoException handled. Continuing...') 12 else: 13 print('-> coroutine received: {!r}'.format(x)) 14 finally: 15 print('-> coroutine ending')
讓協程返回值
下面的 🌰 是 averager 協程的不同版本,這一版會返回結果。為了說明如何返回值,每次激活協程時不會產出移動平均值。這么做是為了強調某些協程不會產出值,而是在最后返回一個值(通常是某種累計值)。
1 from collections import namedtuple 2 3 Result = namedtuple('Result', 'count average') 4 5 def averager(): 6 total = 0.0 7 count = 0 8 average = None 9 while True: 10 term = yield 11 if term is None: # 當send(None)的時候,終止協程 12 break 13 total += term 14 count += 1 15 average = total/count 16 return Result(count, average) # 返回一個namedtuple,包含兩個字段count, average
注意:
return 表達式的值會偷偷傳給調用方,賦值給 StopIteration異常的一個屬性。這樣做有點不合常理,但是能保留生成器對象的常規行為——耗盡時拋出 StopIteration 異常。
演示🌰 捕獲 StopIteration 異常,獲取 averager 返回的值
>>> coro_avg = averager() >>> next(coro_avg) >>> coro_avg.send(10) >>> coro_avg.send(30) >>> coro_avg.send(6.5) >>> try: ... coro_avg.send(None) ... except StopIteration as exc: ... result = exc.value ... >>> result Result(count=3, average=15.5)
使用yield from
首先要知道,yield from 是全新的語言結構。它的作用比 yield 多很多,因此人們認為繼續使用那個關鍵字多少會引起誤解。在其他語言中,類似的結構使用 await 關鍵字,這個名稱好多了,因為它傳達了至關重要的一點:在生成器 gen 中使用 yield from subgen()時,subgen 會獲得控制權,把產出的值傳給 gen 的調用方,即調用方可以直接控制 subgen。與此同時,gen 會阻塞,等待 subgen 終止。
舉個🌰 yield from 可用於簡化 for 循環中的 yield 表達式
1 def gen(): 2 for i in 'AB': 3 yield i 4 5 for i in range(1, 3): 6 yield i 7 8 print(list(gen())) 9 10 11 ''' 12 yield from 版本,可以簡化內部的for循環 13 ''' 14 def gen(): 15 yield from 'AB' 16 yield from range(1, 3) 17 18 print(list(gen()))
🌰 使用 yield from 鏈接可迭代的對象
1 def chain(*iterable): 2 for i in iterable: 3 yield from i 4 5 s = 'ABC' 6 t = tuple(range(1, 5)) 7 r = list(chain(s, t)) 8 print(r)
以上代碼執行的結果為:
['A', 'B', 'C', 1, 2, 3, 4]
yield from x 表達式對 x 對象所做的第一件事是,調用 iter(x),從中獲取迭代器。因此,x 可以是任何可迭代的對象。
yield from 的主要功能是打開雙向通道,把最外層的調用方與最內層的子生成器連接起來,這樣二者可以直接發送和產出值,還可以直接傳入異常,而不用在位於中間的協程中添加大量處理異常的樣板代碼。有了這個結構,協程可以通過以前不可能的方式委托職責。
委派生成器
包含 yield from <iterable> 表達式的生成器函數。
子生成器
從 yield from 表達式中 <iterable> 部分獲取的生成器。
調用方
PEP 380 使用“調用方”這個術語指代調用委派生成器的客戶端代碼。在不同的語境中,我會使用“客戶端”代替“調用方”,以此與委派生成器(也是調用方,因為它調用了子生成器)區分開。
下圖能更好地說明 yield from 結構的用法。圖中把該示例中各個相關的部分標識出來了
委派生成器在 yield from 表達式處暫停時,調用方可以直接把數據發給子生成器,子生成器再把產出的值發給調用方。子生成器返回之后,解釋器會拋出 StopIteration 異常,並把返回值附加到異常對象上,此時委派生成器會恢復
coroaverager3.py 腳本從一個字典中讀取虛構的七年級男女學生的體重和身高。例如, 'boys;m' 鍵對應於 9 個男學生的身高(單位是米),'girls;kg' 鍵對應於 10 個女學生的體重(單位是千克)。這個腳本把各組數據傳給前面定義的 averager 協程,然后生成一個報告,如下所示:
$ python3 coroaverager3.py 9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
🌰 coroaverager3.py:使用 yield from 計算平均值並輸出統計報告
1 from collections import namedtuple 2 3 4 Result = namedtuple('Result', 'count average') 5 6 7 # 子生成器 8 def averager(): # 子生成器 9 total = 0.0 10 count = 0 11 average = None 12 while True: 13 term = yield # 通過main函數中的gourp.send()接收到term的值 14 if term is None: # 至關重要的終止條件,告訴協程所有的數據已經結束,結束協程 15 break 16 total += term 17 count += 1 18 average = total/count 19 return Result(count, average) # 返回 grouper 中yield from的值 20 21 22 # 委派生成器 23 def grouper(results, key): # 委派生成器 24 while True: # 每次循環都會創建一個averager的實例 25 results[key] = yield from averager() # grouper發送的每個值都會讓yield from處理,把產出的值綁定給resuluts[key] 26 27 28 # 客戶端代碼,即調用方 29 def main(data): # main函數是客戶端代碼 30 results = {} 31 for key, values in data.items(): 32 group = grouper(results, key) # group是調用grouper的生成器 33 next(group) # 預激group協程 34 for value in values: 35 group.send(value) # 把各個value的值傳遞給grouper,通過grouper傳入averager中term 36 group.send(None) # 所有值傳遞結束以后,終止averager 37 #print(results) # 如果要調試,去掉注釋 38 report(results) 39 40 #輸出報告 41 def report(results): 42 for key, result in sorted(results.items()): 43 group, unit = key.split(';') 44 print('{:2} {:5} averaging {:.2f}{}'.format( 45 result.count, group, result.average, unit)) 46 47 48 data = { 49 'girls;kg': 50 [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5], 51 'girls;m': 52 [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43], 53 'boys;kg': 54 [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3], 55 'boys;m': 56 [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], 57 } 58 59 if __name__ == '__main__': 60 main(data)
以上代碼執行的結果為:
9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
下面簡要說明上面🌰的運作方式,還會說明把 main 函數中調用group.send(None) 那一行代碼(帶有“重要!”注釋的那一行)去掉會發生什么事。
- 外層 for 循環每次迭代會新建一個 grouper 實例,賦值給 group變量;group 是委派生成器。
- 調用 next(group),預激委派生成器 grouper,此時進入 whileTrue 循環,調用子生成器 averager 后,在 yield from 表達式處暫停。
- 內層 for 循環調用 group.send(value),直接把值傳給子生成器averager。同時,當前的 grouper 實例(group)在 yieldfrom 表達式處暫停。
- 內層循環結束后,group 實例依舊在 yield from 表達式處暫停,因此,grouper 函數定義體中為 results[key] 賦值的語句還沒有執行。
- 如果外層 for 循環的末尾沒有 group.send(None),那么averager 子生成器永遠不會終止,委派生成器 group 永遠不會再次激活,因此永遠不會為 results[key] 賦值。
- 外層 for 循環重新迭代時會新建一個 grouper 實例,然后綁定到group 變量上。前一個 grouper 實例(以及它創建的尚未終止的averager 子生成器實例)被垃圾回收程序回收。
yield from的意義
把迭代器當作生成器使用,相當於把子生成器的定義體內聯在yield from 表達式中。此外,子生成器可以執行 return 語句,返回一個值,而返回的值會成為 yield from 表達式的值
批准后的 PEP 380 在“Proposal”一節(https://www.python.org/dev/peps/pep-0380/#proposal)分六點說明了yield from 的行為。這里,我幾乎原封不動地引述,不過把有歧義的“迭代器”一詞都換成了“子生成器”,還做了進一步說明。示例闡明了下述四點。
- 子生成器產出的值都直接傳給委派生成器的調用方(即客戶端代碼)。
- 使用 send() 方法發給委派生成器的值都直接傳給子生成器。如果發送的值是 None,那么會調用子生成器的 __next__() 方法。如果發送的值不是 None,那么會調用子生成器的 send() 方法。如果調用的方法拋出 StopIteration 異常,那么委派生成器恢復運行。任何其他異常都會向上冒泡,傳給委派生成器。
- 生成器退出時,生成器(或子生成器)中的 return expr 表達式會觸發 StopIteration(expr) 異常拋出。
- yield from 表達式的值是子生成器終止時傳給 StopIteration異常的第一個參數。
yield from 結構的另外兩個特性與異常和終止有關
- 傳入委派生成器的異常,除了 GeneratorExit 之外都傳給子生成器的 throw() 方法。如果調用 throw() 方法時拋出StopIteration 異常,委派生成器恢復運行。StopIteration 之外的異常會向上冒泡,傳給委派生成器。
- 如果把 GeneratorExit 異常傳入委派生成器,或者在委派生成器上調用 close() 方法,那么在子生成器上調用 close() 方法,如果它有的話。如果調用 close() 方法導致異常拋出,那么異常會向上冒泡,傳給委派生成器;否則,委派生成器拋出GeneratorExit 異常。
使用案例:使用協程做離散事件仿真
協程能自然地表述很多算法,例如仿真、游戲、異步 I/O,以及其他事件驅動型編程形式或協作式多任務。
離散事件仿真簡介
離散事件仿真(Discrete Event Simulation,DES)是一種把系統建模成一系列事件的仿真類型。在離散事件仿真中,仿真“鍾”向前推進的量不是固定的,而是直接推進到下一個事件模型的模擬時間。假如我們抽象模擬出租車的運營過程,其中一個事件是乘客上車,下一個事件則是乘客下車。不管乘客坐了 5 分鍾還是 50 分鍾,一旦乘客下車,仿真鍾就會更新,指向此次運營的結束時間。使用離散事件仿真可以在不到一秒鍾的時間內模擬一年的出租車運營過程。這與連續仿真不同,連續仿真的仿真鍾以固定的量(通常很小)不斷向前推進。
顯然,回合制游戲就是離散事件仿真的例子:游戲的狀態只在玩家操作時變化,而且一旦玩家決定下一步怎么走了,仿真鍾就會凍結。而實時游戲則是連續仿真,仿真鍾一直在運行,游戲的狀態在一秒鍾之內更新很多次,因此反應慢的玩家特別吃虧。
這兩種仿真類型都能使用多線程或在單個線程中使用面向事件的編程技術(例如事件循環驅動的回調或協程)實現。可以說,為了實現連續仿真,在多個線程中處理實時並行的操作更自然。而協程恰好為實現離散事件仿真提供了合理的抽象。SimPy 是一個實現離散事件仿真的Python 包,通過一個協程表示離散事件仿真系統中的各個進程。