前言:協程又稱微線程,英文名coroutine。協程是用戶態的一種輕量級線程,是由用戶程序自己控制調度。基於這一原理,協程能在單線程下實現並發。我們知道進程是操作系統分配資源的基本單位,線程是CPU任務調度和執行的最小單位。線程之間的切換是由於線程A遇到了等待操作(比如I/O阻塞)或者計算時間過長,由操作系統控制切換到另一線程B。而協程在遇到阻塞的時候,通過用戶程序切換到另一協程,這個切換過程由程序控制,所以對操作系統來說是無感知的。相比較來說通過程序切換,比操作系統層面的切換,開銷要小的多的多。
協程的優點
- 無需線程上下文切換的開銷
- 無需原子操作(不會被線程調度機制打斷的操作)鎖定以及同步的開銷
- 方便切換控制流,簡化編程模型
- 適合高並發處理場景
協程的缺點
- 無法利用多核資源:協程的本質是單線程,需要和進程配合才能運行在多CPU上
- 進行阻塞(Blocking)操作(如I/O時)會阻塞掉整個程序
一、Gevent 基本使用
Gevent是一種協程的Python網絡庫,基於Greenlet封裝了libevent事件循環的高層同步API。它讓我們在不改變編程習慣的同時,用同步的方式寫異步I/O的代碼。使用Gevent編程性能確實要比用傳統的線程高。
import gevent from gevent import monkey monkey.patch_all() import time,datetime def test(tm): time.sleep(tm) print('時間:{}'.format(datetime.datetime.now())) if __name__ =='__main__': task = [] # 分配10個任務 for i in range(10): task.append(gevent.spawn(test,5)) # 阻塞主線程,直到所有協程運行完成 gevent.joinall(task)
二、monkey.patch_all
用過 gevent 的開發者都知道,在開頭導入monkey.patch_all(),非常重要,會自動將 python 的一些標准模塊替換成 gevent 框架,這個補丁其實存在着一些坑:
- monkey.patch_all(),網上一般叫猴子補丁。如果使用了這個補丁,gevent 直接修改標准庫里面大部分的阻塞式系統調用,包括 socket、ssl、threading 和 select 等模塊,而變為協作式運行。有些地方使用標准庫會由於打了補丁而出現奇怪的問題(比如會影響 multiprocessing 的運行)
- 和一些第三方庫不兼容(比如不能兼容 kazoo)。若要運用到項目中,必須確保其他用到的網絡庫明確支持Gevent。
在實際情況中協程和進程的組合非常常見,兩個結合可以大幅提升性能,但直接使用猴子補丁會導致進程運行出現問題。其實可以按以下辦法解決,將 thread 置成 False,缺點是無法發揮 monkey.patch_all() 的全部性能:
import gevent from gevent import monkey monkey.patch_all(thread=False, socket=False, select=False)
三、Pool 限制並發
協程雖然是輕量級線程,但並發數達到一定量級后,會把系統的文件句柄數占光。所以需要通過 Pool 來限制並發數。
import gevent from gevent.pool import Pool from gevent import monkey monkey.patch_all() import time,datetime def test(tm): time.sleep(tm) print('時間:{}'.format(datetime.datetime.now())) if __name__ =='__main__': task = [] # 限制最大並發 pool = Pool(5) # 分配100個任務,最大並發數為5 for i in range(100): task.append(pool.spawn(test,5)) gevent.joinall(task)
運行結果:
時間:2020-11-20 17:08:15.625334 時間:2020-11-20 17:08:15.625334 時間:2020-11-20 17:08:15.625334 時間:2020-11-20 17:08:15.625334 時間:2020-11-20 17:08:15.625334 時間:2020-11-20 17:08:20.626347 時間:2020-11-20 17:08:20.626347 時間:2020-11-20 17:08:20.626347 時間:2020-11-20 17:08:20.626347 時間:2020-11-20 17:08:20.626347 時間:2020-11-20 17:08:25.627630 時間:2020-11-20 17:08:25.627630 時間:2020-11-20 17:08:25.627630 時間:2020-11-20 17:08:25.627630 時間:2020-11-20 17:08:25.627630 。。。
結語:gevent 雖然在編程方面很方便,開頭使用 monkey.patch_all(),就能讓你的同步代碼享受到異步的性能。但坑也是存在的,所以復雜的業務場景不推薦使用 gevent,可以使用python 標准庫中的 asyncio。