2021年2月17日 星期三

[ Python 文章收集 ] 深入了解 gevent

 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 實例進行執行。一個簡單的例子如下所示:
  1. from greenlet import greenlet  
  2.   
  3. def test1():  
  4.     print(12)  
  5.     gr2.switch()  
  6.     print(34)  
  7.   
  8. def test2():  
  9.     print(56)  
  10.     gr1.switch()  
  11.     print(78)  
  12.   
  13. gr1 = greenlet(test1)  
  14. gr2 = greenlet(test2)  
  15. gr1.switch()  
上面的代碼執行的結果是:
12
56
34

可以看到上面創建了兩個 greenlet 實例 gr1gr2,首先最後一行調用 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)。下面介紹一個簡單實例:
  1. from greenlet import greenlet  
  2.   
  3. def test1():  
  4.     print(12)  
  5.     gr2.switch()  
  6.     print(34)  
  7.   
  8. def test2():  
  9.     print(56)  
  10.     print(78)  
  11.   
  12. gr1 = greenlet(test1)  
  13. gr2 = greenlet(test2, parent=gr1)  
  14. gr1.switch()  
可以看到上面的代碼執行的結果如下所示:
12
56
78
34

可以看到建立了兩個 greenlet gr1gr2,其中 gr2 的父 greenlet 是 gr1gr1 沒有指定父 greenlet,因此默認是主 greenlet。實際執行時,首先啟動 gr1,打印12,之後切換為 gr2, 打印 56,78,之後 gr2 執行結束,切換為父 greenlet gr1,從切換處繼續執行,打印 34,之後 gr1 執行結束,切換為主 greenlet ,繼續往下,主 greenlet 也執行結束。

greenlet 應用
可以看到 greenlet 思路很清晰,協程的切換接口很易用。但是對於實際的業務開發依舊存在一些不便之處:
1. greenlet 原始的執行方法都需要轉換為 greenlet 實例,而且需要使用者管理 greenlet 實例之間的樹形關係,對於業務開發而言並不友好;
2. greenlet 的切換需要調用 greenlet.switch() 方法進行切換,業務中需要充斥大量的 greenlet 管理代碼;

gevent
gevent 基於 greenlet 庫進行了封裝,基於 libev 和 libuv 提供了高效的同步API。 對greenlet 在業務開發中的不便之處,提供了很好的解決方案:
1. 對於greenlet 實例的管理,不使用樹形關係進行組織,隱藏不必要的複雜性;
2. 採用 monkey patching 與第三方庫協作,不需要重寫原因方法,也不需要手工通過 greenlet.switch() 切換;

基本使用類似如下所示:
  1. impo8rt gevent  
  2. from gevent import socket  
  3.   
  4. urls = ['www.google.com''www.example.com''www.python.org']  
  5. jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]  
  6. gevent.joinall(jobs, timeout=2)  
  7.   
  8. print([job.value for job in jobs])  
上面的示例中使用了gevent 中兩個基礎接口:
* gevent.spawn() 此方法用於創建一個 greenlet 實例,用於執行特定的方法
* gevent.joinall() 用於等待所有的 greenlet 實例執行完畢

上面的例子中創建了三個 greenlet 實例,執行 socket.gethostbyname 方法,全部執行結束後獲取請求的結果。可以看到業務代碼使用 gevent 時,不需要關心 greenlet 實例組織的邏輯,由 gevent 統一組織調度起來。

monkey patching
通過之前的介紹,gevent 是利用協程實現線程的複用,在子任務出現 I/O 操作時,切換去執行其他子任務,但是在業務代碼中,不會手工觸發 greenlet.switch() 去觸發切換到另一個子任務,甚至默認的網絡庫進行網絡請求時是阻塞式地請求網絡,完全沒有辦法切換子任務去實現復用,那麼 gevent 是如何實現的呢?

答案是 monkey patching,gevent.monkey 模塊提供了大量的方法與類去替換第三方阻塞式的庫的行為,比如替換 socket 庫中的行為從而支持非阻塞式的網絡請求。比如希望引入非阻塞式的網絡請求,可以實現的方式如下:

1. 直接從 gevent 中引入socket 庫進行使用,代碼如下所示
  1. from gevent import socket  
2. 使用系統的 socket 庫,並使用 gevent 的 monkey.patch_socket() 對其打補丁
  1. from gevent import monkey; monkey.patch_socket()  
如果希望直接用gevent 對其所支持的第三方庫打補丁,使其轉換為非阻塞的操作,可以使用如下所示的代碼
  1. from gevent import monkey; monkey.patch_all()  
gevent 應用
通過上面的介紹可以看到,gevent 通過接口的封裝,讓使用者不需要關心greenlet 實例的調度,易用性得到提升,同時利用monkey patching,讓使用者不用關心任務的切換,從而讓業務代碼寫起來更加方便.

gunicorn 與 gevent
在實際的 web 部署中,我們的使用 gevent 比前面介紹得要更加簡單一些,我們甚至都不需要顯示去執行 monkey.patch_all() 去給第三方庫打補丁,直接在 gunicorn 啟動 web 服務時,通過 -k gevent去指定工作進程類型即可,後續不需要任何的開發。看起來 gunicorn 幫助我們做了該做的初始化,具體代碼是怎麼呢?在 github 的項目的 gunicorn/workers/ggevent.py 可以看到相關的實現:
  1. def patch(self):  
  2.     monkey.patch_all()  
  3.   
  4.     # monkey patch sendfile to make it none blocking       
  5.     patch_sendfile()  
  6.   
  7.     # patch sockets       
  8.     sockets = []  
  9.     for s in self.sockets:  
  10.         sockets.append(socket.socket(s.FAMILY, socket.SOCK_STREAM, fileno=s.sock.fileno()))  
  11.     self.sockets = sockets  
  12.   
  13.   
  14. def init_process(self):  
  15.     self.patch()  
  16.     super().init_process()  
可以看到 gunicorn 的初始化工作進程中,調用 self.patch() 方法,執行了 monkey.patch_all() 給第三方庫加上了補丁,從而保證在使用 gevent 時,可以將支持的第三方的阻塞方法轉換為非阻塞方法,從而充分利用協程提供的並發效率。

Supplement
gevent For the Working Python Developer

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...