Source From Here
背景介紹在 python 的 web 部署中,經常會使用 gunicorn 啟動 web 服務,同時,為了並發效率更高,一般會使用 -w 指定多個工作進程 (worker processes) ,同時可以通過 -k 指定工作進程的類型,目前支持的工作進程的類型包括: sync, eventlet, gevent, tornado, gthread。具體選擇工作進程類型可以參考此 博客。在我們的項目中,主要使用 gevent,本篇文章就希望能深入介紹 gevent 在 web 部署中是如何工作以及提升效率的。
協程
協程 也被稱為微線程,是一種比線程更輕量級的任務調度方式,一個線程內可以有多個協程。
協程是一種可以在子程序內部中斷,轉而執行其他子程序,之後再從中斷點繼續執行的機制。比如在 I/O 操作時就可以執行其他子程序,等 I/O 操作數據就緒時繼續執行,就可以在單個線程內實現非阻塞式的複用,大大提升效率。
greenlet
greenlet 是一個輕量級的協程實現,使用的方法簡單而清晰。創建 greenlet 實例執行方法,在方法內部可通過 greenlet.switch() 切換至其他 greenlet 實例進行執行。一個簡單的例子如下所示:
- from greenlet import greenlet
- def test1():
- print(12)
- gr2.switch()
- print(34)
- def test2():
- print(56)
- gr1.switch()
- print(78)
- gr1 = greenlet(test1)
- gr2 = greenlet(test2)
- gr1.switch()
可以看到上面創建了兩個 greenlet 實例 gr1, gr2,首先最後一行調用 gr1.switch() 啟動執行 test1 方法,打印出 12,接著執行 gr2.switch() 從而啟動執行 test2 方法,打印出 56,接著執行 gr1.swich() 方法執行 test1方法,從上次的斷點出繼續執行,打印出 34,之後就結束了,因此最終 78 不會被打印出來。
可以看到 greenlet 實例的運行邏輯很簡單,就是切換時保存現場,下一通過 gevent.switch() 切換回來時,從切換點繼續執行。但是並不是所有的 gevent 實例都能執行結束,比如上面的 gr2 就沒有執行結束,因為沒有切換回來。
父子關係
greenlet 實例之間存在父子關係,當子 greenlet 執行完畢後,父 greenlet 繼續執行。在創建 greenlet 時,可以指定其父 greenlet,如果不指定父 greenlet, 那麼其父 greenlet 就是主 greenlet(main greenlet)。下面介紹一個簡單實例:
- from greenlet import greenlet
- def test1():
- print(12)
- gr2.switch()
- print(34)
- def test2():
- print(56)
- print(78)
- gr1 = greenlet(test1)
- gr2 = greenlet(test2, parent=gr1)
- gr1.switch()
可以看到建立了兩個 greenlet gr1, gr2,其中 gr2 的父 greenlet 是 gr1,gr1 沒有指定父 greenlet,因此默認是主 greenlet。實際執行時,首先啟動 gr1,打印12,之後切換為 gr2, 打印 56,78,之後 gr2 執行結束,切換為父 greenlet gr1,從切換處繼續執行,打印 34,之後 gr1 執行結束,切換為主 greenlet ,繼續往下,主 greenlet 也執行結束。
greenlet 應用
可以看到 greenlet 思路很清晰,協程的切換接口很易用。但是對於實際的業務開發依舊存在一些不便之處:
gevent
gevent 基於 greenlet 庫進行了封裝,基於 libev 和 libuv 提供了高效的同步API。 對greenlet 在業務開發中的不便之處,提供了很好的解決方案:
基本使用類似如下所示:
- impo8rt gevent
- from gevent import socket
- urls = ['www.google.com', 'www.example.com', 'www.python.org']
- jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
- gevent.joinall(jobs, timeout=2)
- print([job.value for job in jobs])
上面的例子中創建了三個 greenlet 實例,執行 socket.gethostbyname 方法,全部執行結束後獲取請求的結果。可以看到業務代碼使用 gevent 時,不需要關心 greenlet 實例組織的邏輯,由 gevent 統一組織調度起來。
monkey patching
通過之前的介紹,gevent 是利用協程實現線程的複用,在子任務出現 I/O 操作時,切換去執行其他子任務,但是在業務代碼中,不會手工觸發 greenlet.switch() 去觸發切換到另一個子任務,甚至默認的網絡庫進行網絡請求時是阻塞式地請求網絡,完全沒有辦法切換子任務去實現復用,那麼 gevent 是如何實現的呢?
答案是 monkey patching,gevent.monkey 模塊提供了大量的方法與類去替換第三方阻塞式的庫的行為,比如替換 socket 庫中的行為從而支持非阻塞式的網絡請求。比如希望引入非阻塞式的網絡請求,可以實現的方式如下:
1. 直接從 gevent 中引入socket 庫進行使用,代碼如下所示
- from gevent import socket
- from gevent import monkey; monkey.patch_socket()
- from gevent import monkey; monkey.patch_all()
通過上面的介紹可以看到,gevent 通過接口的封裝,讓使用者不需要關心greenlet 實例的調度,易用性得到提升,同時利用monkey patching,讓使用者不用關心任務的切換,從而讓業務代碼寫起來更加方便.
gunicorn 與 gevent
在實際的 web 部署中,我們的使用 gevent 比前面介紹得要更加簡單一些,我們甚至都不需要顯示去執行 monkey.patch_all() 去給第三方庫打補丁,直接在 gunicorn 啟動 web 服務時,通過 -k gevent去指定工作進程類型即可,後續不需要任何的開發。看起來 gunicorn 幫助我們做了該做的初始化,具體代碼是怎麼呢?在 github 的項目的 gunicorn/workers/ggevent.py 可以看到相關的實現:
- def patch(self):
- monkey.patch_all()
- # monkey patch sendfile to make it none blocking
- patch_sendfile()
- # patch sockets
- sockets = []
- for s in self.sockets:
- sockets.append(socket.socket(s.FAMILY, socket.SOCK_STREAM, fileno=s.sock.fileno()))
- self.sockets = sockets
- def init_process(self):
- self.patch()
- super().init_process()
Supplement
* gevent For the Working Python Developer
沒有留言:
張貼留言