1. 程式人生 > >Day41:協程

Day41:協程

默認 思想 註意 lib 創建 隊列 任務 文件描述 star

一、協程

協程,又稱微線程,纖程。英文名Coroutine。一句話說明什麽是線程:協程是一種用戶態的輕量級線程。

協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此:

協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當於進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。

1.1 yield與協程

import time

"""
傳統的生產者-消費者模型是一個線程寫消息,一個線程取消息,通過鎖機制控制隊列和等待,但一不小心就可能死鎖。
如果改用協程,生產者生產消息後,直接通過yield跳轉到消費者開始執行,待消費者執行完畢後,切換回生產者繼續生產,效率極高。
""" # 註意到consumer函數是一個generator(生成器): # 任何包含yield關鍵字的函數都會自動成為生成器(generator)對象 def consumer(): r = ‘‘ while True: # 3、consumer通過yield拿到消息,處理,又通過yield把結果傳回; # yield指令具有return關鍵字的作用。然後函數的堆棧會自動凍結(freeze)在這一行。 # 當函數調用者的下一次利用next()或generator.send()或for-in來再次調用該函數時, #
就會從yield代碼的下一行開始,繼續執行,再返回下一次叠代結果。通過這種方式,叠代器可以實現無限序列和惰性求值。 n = yield r if not n: return print([CONSUMER] ←← Consuming %s... % n) time.sleep(1) r = 200 OK def produce(c): # 1、首先調用c.next()啟動生成器 next(c) n = 0 while n < 5: n
= n + 1 print([PRODUCER] →→ Producing %s... % n) # 2、然後,一旦生產了東西,通過c.send(n)切換到consumer執行; cr = c.send(n) # 4、produce拿到consumer處理的結果,繼續生產下一條消息; print([PRODUCER] Consumer return: %s % cr) # 5、produce決定不生產了,通過c.close()關閉consumer,整個過程結束。 c.close() if __name__==__main__: # 6、整個流程無鎖,由一個線程執行,produce和consumer協作完成任務,所以稱為“協程”,而非線程的搶占式多任務。 c = consumer() produce(c) ‘‘‘ result: [PRODUCER] →→ Producing 1... [CONSUMER] ←← Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 2... [CONSUMER] ←← Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 3... [CONSUMER] ←← Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 4... [CONSUMER] ←← Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 5... [CONSUMER] ←← Consuming 5... [PRODUCER] Consumer return: 200 OK ‘‘‘

1.2 greenlet

greenlet機制的主要思想是:生成器函數或者協程函數中的yield語句掛起函數的執行,直到稍後使用next()或send()操作進行恢復為止。可以使用一個調度器循環在一組生成器函數之間協作多個任務。greentlet是python中實現我們所謂的"Coroutine(協程)"的一個基礎庫。

from greenlet import greenlet
 
def test1():
    print (12)
    gr2.switch()
    print (34)
    gr2.switch()
 
def test2():
    print (56)
    gr1.switch()
    print (78)
 
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

1.3 gevent模塊實現協程

Python通過yield提供了對協程的基本支持,但是不完全。而第三方的gevent為Python提供了比較完善的協程支持。

gevent是第三方庫,通過greenlet實現協程,其基本思想是:

當一個greenlet遇到IO操作時,比如訪問網絡,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO。

由於切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,這一過程在啟動時通過monkey patch完成:

import gevent
import time

def foo():
    print("running in foo")
    gevent.sleep(2)
    print("switch to foo again")

def bar():
    print("switch to bar")
    gevent.sleep(5)
    print("switch to bar again")

start=time.time()

gevent.joinall(
    [gevent.spawn(foo),
    gevent.spawn(bar)]
)

print(time.time()-start)

當然,實際代碼裏,我們不會用gevent.sleep()去切換協程,而是在執行到IO操作時,gevent自動切換,代碼如下:

from gevent import monkey
monkey.patch_all()
import gevent
from urllib import request
import time

def f(url):
    print(GET: %s % url)
    resp = request.urlopen(url)
    data = resp.read()
    print(%d bytes received from %s. % (len(data), url))

start=time.time()

gevent.joinall([
        gevent.spawn(f, https://itk.org/),
        gevent.spawn(f, https://www.github.com/),
        gevent.spawn(f, https://zhihu.com/),
])

# f(‘https://itk.org/‘)
# f(‘https://www.github.com/‘)
# f(‘https://zhihu.com/‘)

print(time.time()-start)
技術分享
===========================一


gevent是一個基於協程(coroutine)的Python網絡函數庫,通過使用greenlet提供了一個在libev事件循環頂部的高級別並發API。

主要特性有以下幾點:

<1> 基於libev的快速事件循環,Linux上面的是epoll機制

<2> 基於greenlet的輕量級執行單元

<3> API復用了Python標準庫裏的內容

<4> 支持SSL的協作式sockets

<5> 可通過線程池或c-ares實現DNS查詢

<6> 通過monkey patching功能來使得第三方模塊變成協作式

gevent.spawn()方法spawn一些jobs,然後通過gevent.joinall將jobs加入到微線程執行隊列中等待其完成,設置超時為2秒。執行後的結果通過檢查gevent.Greenlet.value值來收集。


===========================二
1、關於Linux的epoll機制:

epoll是Linux內核為處理大批量文件描述符而作了改進的poll,是Linux下多路復用IO接口select/poll的
增強版本,它能顯著提高程序在大量並發連接中只有少量活躍的情況下的系統CPU利用率。epoll的優點:

(1)支持一個進程打開大數目的socket描述符。select的一個進程所打開的FD由FD_SETSIZE的設置來限定,而epoll沒有這個限制,它所支持的FD上限是
最大可打開文件的數目,遠大於2048。

(2)IO效率不隨FD數目增加而線性下降:由於epoll只會對“活躍”的socket進行操作,於是,只有”活躍”的socket才會主動去調用 callback函數,其他
idle狀態的socket則不會。

(3)使用mmap加速內核與用戶空間的消息傳遞。epoll是通過內核於用戶空間mmap同一塊內存實現的。

(4)內核微調。

2、libev機制

提供了指定文件描述符事件發生時調用回調函數的機制。libev是一個事件循環器:向libev註冊感興趣的事件,比如socket可讀事件,libev會對所註冊的事件
的源進行管理,並在事件發生時觸發相應的程序。

===========================三


‘’‘

import gevent

            from gevent import socket

            urls = [‘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跟隨著無阻的請求而執行。

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共同工作的目的。

事件循環

不像其他網絡庫,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。
擴展

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 阻塞無關的大運算量耗時操作基本沒有什麽幫助。

1.4 總結

協程的好處:

無需線程上下文切換的開銷
無需原子操作鎖定及同步的開銷
方便切換控制流,簡化編程模型
高並發+高擴展性+低成本:一個CPU支持上萬的協程都不是問題。所以很適合用於高並發處理。
缺點:

無法利用多核資源:協程的本質是個單線程,它不能同時將 單個CPU 的多個核用上,協程需要和進程配合才能運行在多CPU上.當然我們日常所編寫的絕大部分應用都沒有這個必要,除非是cpu密集型應用。
進行阻塞(Blocking)操作(如IO時)會阻塞掉整個程序。

Day41:協程