Python 协程gevent
gevent是第三方庫,通過greenlet實現協程,其基本思想是:
當一個greenlet遇到IO操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由于IO操作非常耗時,經常使程序處于等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO。
由于切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,這一過程在啟動時通過monkey patch完成:
from gevent import monkey; monkey.patch_socket() import gevent import timedef f(n):for i in range(n):gevent.sleep(1)print gevent.getcurrent(), ig1 = gevent.spawn(f, 5) g2 = gevent.spawn(f, 5) g3 = gevent.spawn(f, 5) g1.join() g2.join() g3.join()運行結果:
<Greenlet at 0x2134d91b9d0: f(5)> 0 <Greenlet at 0x2134dd91e10: f(5)> 1 <Greenlet at 0x2134dd91bf0: f(5)> 2 <Greenlet at 0x2134d91b9d0: f(5)> 3 <Greenlet at 0x2134dd91e10: f(5)> 4 <Greenlet at 0x2134dd91bf0: f(5)> 0 <Greenlet at 0x2134d91b9d0: f(5)> 1 <Greenlet at 0x2134dd91e10: f(5)> 2 <Greenlet at 0x2134dd91bf0: f(5)> 3 <Greenlet at 0x2134d91b9d0: f(5)> 4 <Greenlet at 0x2134dd91e10: f(5)> 0 <Greenlet at 0x2134dd91bf0: f(5)> 1 <Greenlet at 0x2134d91b9d0: f(5)> 2 <Greenlet at 0x2134dd91e10: f(5)> 3 <Greenlet at 0x2134dd91bf0: f(5)> 4可以看到,3個greenlet是依次運行而不是交替運行。
要讓greenlet交替運行,可以通過gevent.sleep()交出控制權:
def f(n):for i in range(n):print gevent.getcurrent(), igevent.sleep(0)執行結果:
<Greenlet at 0x10cd58550: f(5)> 0 <Greenlet at 0x10cd58910: f(5)> 0 <Greenlet at 0x10cd584b0: f(5)> 0 <Greenlet at 0x10cd58550: f(5)> 1 <Greenlet at 0x10cd584b0: f(5)> 1 <Greenlet at 0x10cd58910: f(5)> 1 <Greenlet at 0x10cd58550: f(5)> 2 <Greenlet at 0x10cd58910: f(5)> 2 <Greenlet at 0x10cd584b0: f(5)> 2 <Greenlet at 0x10cd58550: f(5)> 3 <Greenlet at 0x10cd584b0: f(5)> 3 <Greenlet at 0x10cd58910: f(5)> 3 <Greenlet at 0x10cd58550: f(5)> 4 <Greenlet at 0x10cd58910: f(5)> 4 <Greenlet at 0x10cd584b0: f(5)> 43個greenlet交替運行,
把循環次數改為500000,讓它們的運行時間長一點,然后在操作系統的進程管理器中看,線程數只有1個。
當然,實際代碼里,我們不會用gevent.sleep()去切換協程,而是在執行到IO操作時,gevent自動切換,代碼如下:
from gevent import monkey; monkey.patch_all() import gevent import urllib2def f(url):print('GET: %s' % url)resp = urllib2.urlopen(url)data = resp.read()print('%d bytes received from %s.' % (len(data), url))gevent.joinall([gevent.spawn(f, 'https://www.python.org/'),gevent.spawn(f, 'https://www.yahoo.com/'),gevent.spawn(f, 'https://github.com/'), ])運行結果:
GET: https://www.python.org/ GET: https://www.yahoo.com/ GET: https://github.com/ 45661 bytes received from https://www.python.org/. 14823 bytes received from https://github.com/. 304034 bytes received from https://www.yahoo.com/.從結果看,3個網絡操作是并發執行的,而且結束順序不同,但只有一個線程。
?
1 關于greenlet
greelet指的是使用一個任務調度器和一些生成器或者協程實現協作式用戶空間多線程的一種偽并發機制,即所謂的微線程。
greelet機制的主要思想是:生成器函數或者協程函數中的yield語句掛起函數的執行,直到稍后使用next()或send()操作進行恢復為止。可以使用一個調度器循環在一組生成器函數之間協作多個任務。
網絡框架的幾種基本的網絡I/O模型:
阻塞式單線程:這是最基本的I/O模型,只有在處理完一個請求之后才會處理下一個請求。它的缺點是效能差,如果有請求阻塞住,會讓服務無法繼續接受請求。但是這種模型編寫代碼相對簡單,在應對訪問量不大的情況時是非常適合的。
阻塞式多線程:針對于單線程接受請求量有限的缺點,一個很自然的想法就是給每一個請求開一個線程去處理。這樣做的好處是能夠接受更多的請求,缺點是在線程產生到一定數量之后,進程之間需要大量進行切換上下文的操作,會占用CPU大量的時間,不過這樣處理的話編寫代碼的難道稍高于單進程的情況。
非阻塞式事件驅動:為了解決多線程的問題,有一種做法是利用一個循環來檢查是否有網絡IO的事件發生,以便決定如何來進行處理(reactor設計模式)。這樣的做的好處是進一步降低了CPU的資源消耗。缺點是這樣做會讓程序難以編寫,因為請求接受后的處理過程由reactor來決定,使得程序的執行流程難以把握。當接受到一個請求后如果涉及到阻塞的操作,這個請求的處理就會停下來去接受另一個請求,程序執行的流程不會像線性程序那樣直觀。twisted框架就是應用這種IO模型的典型例子。
非阻塞式Coroutine(協程):這個模式是為了解決事件驅動模型執行流程不直觀的問題,它在本質上也是事件驅動的,加入了Coroutine的概念。
2 與線程/進程的區別
線程是搶占式的調度,多個線程并行執行,搶占共同的系統資源;而微線程是協同式的調度。
其實greenlet不是一種真正的并發機制,而是在同一線程內,在不同函數的執行代碼塊之間切換,實施“你運行一會、我運行一會”,并且在進行切換時必須指定何時切換以及切換到哪。greenlet的接口是比較簡單易用的,但是使用greenlet時的思考方式與其他并發方案存在一定區別:
線程/進程模型在大邏輯上通常從并發角度開始考慮,把能夠并行處理的并且值得并行處理的任務分離出來,在不同的線程/進程下運行,然后考慮分離過程可能造成哪些互斥、沖突問題,將互斥的資源加鎖保護來保證并發處理的正確性。
greenlet則是要求從避免阻塞的角度來進行開發,當出現阻塞時,就顯式切換到另一段沒有被阻塞的代碼段執行,直到原先的阻塞狀況消失以后,再人工切換回原來的代碼段繼續處理。因此,greenlet本質是一種合理安排了的 串行 。
greenlet本質是串行,因此在沒有進行顯式切換時,代碼的其他部分是無法被執行到的,如果要避免代碼長時間占用運算資源造成程序假死,那么還是要將greenlet與線程/進程機制結合使用(每個線程、進程下都可以建立多個greenlet,但是跨線程/進程時greenlet之間無法切換或通訊)。
3 使用
一個 “greenlet” 是一個很小的獨立微線程。可以把它想像成一個堆棧幀,棧底是初始調用,而棧頂是當前greenlet的暫停位置。你使用greenlet創建一堆這樣的堆棧,然后在他們之間跳轉執行。跳轉不是絕對的:一個greenlet必須選擇跳轉到選擇好的另一個greenlet,這會讓前一個掛起,而后一個恢復。兩 個greenlet之間的跳轉稱為 切換(switch) 。
當你創建一個greenlet,它得到一個初始化過的空堆棧;當你第一次切換到它,他會啟動指定的函數,然后切換跳出greenlet。當最終棧底 函數結束時,greenlet的堆棧又編程空的了,而greenlet也就死掉了。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()最后一行跳轉到 test1() ,它打印12,然后跳轉到 test2() ,打印56,然后跳轉回 test1() ,打印34,然后 test1() 就結束,gr1死掉。這時執行會回到原來的 gr1.switch() 調用。注意,78是不會被打印的,因為gr1已死,不會再切換。
4 基于greenlet的框架
4.1 eventlet
eventlet 是基于 greenlet 實現的面向網絡應用的并發處理框架,提供“線程”池、隊列等與其他 Python 線程、進程模型非常相似的 api,并且提供了對 Python 發行版自帶庫及其他模塊的超輕量并發適應性調整方法,比直接使用 greenlet 要方便得多。
其基本原理是調整 Python 的 socket 調用,當發生阻塞時則切換到其他 greenlet 執行,這樣來保證資源的有效利用。需要注意的是:
eventlet 提供的函數只能對 Python 代碼中的 socket 調用進行處理,而不能對模塊的 C 語言部分的 socket 調用進行修改。對后者這類模塊,仍然需要把調用模塊的代碼封裝在 Python 標準線程調用中,之后利用 eventlet 提供的適配器實現 eventlet 與標準線程之間的協作。
雖然 eventlet 把 api 封裝成了非常類似標準線程庫的形式,但兩者的實際并發執行流程仍然有明顯區別。在沒有出現 I/O 阻塞時,除非顯式聲明,否則當前正在執行的 eventlet 永遠不會把 cpu 交給其他的 eventlet,而標準線程則是無論是否出現阻塞,總是由所有線程一起爭奪運行資源。所有 eventlet 對 I/O 阻塞無關的大運算量耗時操作基本沒有什么幫助。
4.2 gevent
4.2.1 gevent是一個基于協程(coroutine)的Python網絡函數庫,通過使用greenlet提供了一個在libev事件循環頂部的高級別并發API。
主要特性有以下幾點:
基于libev的快速事件循環,Linux上面的是epoll機制
基于greenlet的輕量級執行單元
API復用了Python標準庫里的內容
支持SSL的協作式sockets
可通過線程池或c-ares實現DNS查詢
通過monkey patching功能來使得第三方模塊變成協作式
ps:
1、關于Linux的epoll機制:
epoll是Linux內核為處理大批量文件描述符而作了改進的poll,是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量并發連接中只有少量活躍的情況下的系統CPU利用率。epoll的優點:
支持一個進程打開大數目的socket描述符。select的一個進程所打開的FD由FD_SETSIZE的設置來限定,而epoll沒有這個限制,它所支持的FD上限是最大可打開文件的數目,遠大于2048。
IO效率不隨FD數目增加而線性下降:由于epoll只會對“活躍”的socket進行操作,于是,只有”活躍”的socket才會主動去調用 callback函數,其他idle狀態的socket則不會。
使用mmap加速內核與用戶空間的消息傳遞。epoll是通過內核于用戶空間mmap同一塊內存實現的。
內核微調。
2、libev機制
提供了指定文件描述符事件發生時調用回調函數的機制。libev是一個事件循環器:向libev注冊感興趣的事件,比如socket可讀事件,libev會對所注冊的事件的源進行管理,并在事件發生時觸發相應的程序。
4.2.2 官方文檔中的示例:
import geventfrom gevent import socketurls = [‘www.google.com.hk’,’www.example.com’, ‘www.python.org’ ]jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]gevent.joinall(jobs, timeout=2)[job.value for job in jobs][‘74.125.128.199’, ‘208.77.188.166’, ‘82.94.164.162’]
注解:gevent.spawn()方法spawn一些jobs,然后通過gevent.joinall將jobs加入到微線程執行隊列中等待其完成,設置超時為2秒。執行后的結果通過檢查gevent.Greenlet.value值來收集。gevent.socket.gethostbyname()函數與標準的socket.gethotbyname()有相同的接口,但它不會阻塞整個解釋器,因此會使得其他的greenlets跟隨著無阻的請求而執行。
4.2.3 Monket patching
Python的運行環境允許我們在運行時修改大部分的對象,包括模塊、類甚至函數。雖然這樣做會產生“隱式的副作用”,而且出現問題很難調試,但在需要修改Python本身的基礎行為時,Monkey patching就派上用場了。Monkey patching能夠使得gevent修改標準庫里面大部分的阻塞式系統調用,包括socket,ssl,threading和select等模塊,而變成協作式運行。
from gevent import monkey ;monkey . patch_socket ()import urllib2通過monkey.patch_socket()方法,urllib2模塊可以使用在多微線程環境,達到與gevent共同工作的目的。
4.2.4 事件循環
不像其他網絡庫,gevent和eventlet類似, 在一個greenlet中隱式開始事件循環。沒有必須調用run()或dispatch()的反應器(reactor),在twisted中是有 reactor的。當gevent的API函數想阻塞時,它獲得Hub實例(執行時間循環的greenlet),并切換過去。如果沒有集線器實例則會動態 創建。
libev提供的事件循環默認使用系統最快輪詢機制,設置LIBEV_FLAGS環境變量可指定輪詢機制。LIBEV_FLAGS=1為select, LIBEV_FLAGS = 2為poll, LIBEV_FLAGS = 4為epoll,LIBEV_FLAGS = 8為kqueue。
Libev的API位于gevent.core下。注意libev API的回調在Hub的greenlet運行,因此使用同步greenlet的API。可以使用spawn()和Event.set()等異步API。
總結
以上是生活随笔為你收集整理的Python 协程gevent的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# 保留两位小数
- 下一篇: websocket python爬虫_p