您好,登錄后才能下訂單哦!
Python如何協程?很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
通常在Python中我們進行并發編程一般都是使用多線程或者多進程來實現的,對于計算型任務由于GIL的存在我們通常使用多進程來實現,而對于IO型任務我們可以通過線程調度來讓線程在執行IO任務時讓出GIL,從而實現表面上的并發。其實對于IO型任務我們還有一種選擇就是協程,協程是運行在單線程當中的"并發",協程相比多線程一大優勢就是省去了多線程之間的切換開銷,獲得了更大的運行效率。
協程,又稱微線程,纖程,英文名Coroutine。協程的作用是在執行函數A時可以隨時中斷去執行函數B,然后中斷函數B繼續執行函數A(可以自由切換)。但這一過程并不是函數調用,這一整個過程看似像多線程,然而協程只有一個線程執行。
那協程有什么優勢呢?
執行效率極高,因為子程序切換(函數)不是線程切換,由程序自身控制,沒有切換線程的開銷。所以與多線程相比,線程的數量越多,協程性能的優勢越明顯。
不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在控制共享資源時也不需要加鎖,因此執行效率高很多。
協程可以處理IO密集型程序的效率問題,但是處理CPU密集型不是它的長處,如要充分發揮CPU利用率可以結合多進程+協程。
Python中的協程經歷了很長的一段發展歷程。其大概經歷了如下三個階段:
最初的生成器變形yield/send
引入@asyncio.coroutine和yield from
引入async/await關鍵字
上述是協程概念和優勢的一些簡介,感覺會比較抽象,Python2.x對協程的支持比較有限,生成器yield實現了一部分但不完全,gevent模塊倒是有比較好的實現;Python3.4加入了asyncio模塊,在Python3.5中又提供了async/await語法層面的支持,Python3.6中asyncio模塊更加完善和穩定。接下來我們圍繞這些內容詳細闡述一下。
Python2.x協程
python2.x實現協程的方式有:
yield + send
gevent (見后續章節)
yield + send(利用生成器實現協程)
我們通過“生產者-消費者”模型來看一下協程的應用,生產者生產消息后,直接通過yield跳轉到消費者開始執行,待消費者執行完畢后,切換回生產者繼續生產。
#-*- coding:utf8 -*- def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER]Consuming %s...' % n) r = '200 OK' def producer(c): # 啟動生成器 c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER]Producing %s...' % n) r = c.send(n) print('[PRODUCER]Consumer return: %s' % r) c.close() if __name__ == '__main__': c = consumer() producer(c)
復制代碼send(msg)與next()的區別在于send可以傳遞參數給yield表達式,這時傳遞的參數會作為yield表達式的值,而yield的參數是返回給調用者的值。換句話說,就是send可以強行修改上一個yield表達式的值。比如函數中有一個yield賦值a = yield 5,第一次迭代到這里會返回5,a還沒有賦值。第二次迭代時,使用send(10),那么就是強行修改yield 5表達式的值為10,本來是5的,結果a = 10。send(msg)與next()都有返回值,它們的返回值是當前迭代遇到yield時,yield后面表達式的值,其實就是當前迭代中yield后面的參數。第一次調用send時必須是send(None),否則會報錯,之所以為None是因為這時候還沒有一個yield表達式可以用來賦值。上述例子運行之后輸出結果如下:
[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
Python3.x協程
除了Python2.x中協程的實現方式,Python3.x還提供了如下方式實現協程:
asyncio + yield from (python3.4+)
asyncio + async/await (python3.5+)
Python3.4以后引入了asyncio模塊,可以很好的支持協程。
asyncio + yield from
asyncio是Python3.4版本引入的標準庫,直接內置了對異步IO的支持。asyncio的異步操作,需要在coroutine中通過yield from完成。看如下代碼(需要在Python3.4以后版本使用):
#-*- coding:utf8 -*- import asyncio @asyncio.coroutine def test(i): print('test_1', i) r = yield from asyncio.sleep(1) print('test_2', i) if __name__ == '__main__': loop = asyncio.get_event_loop() tasks = [test(i) for i in range(3)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
@asyncio.coroutine把一個generator標記為coroutine類型,然后就把這個coroutine扔到EventLoop中執行。test()會首先打印出test_1,然后yield from語法可以讓我們方便地調用另一個generator。由于asyncio.sleep()也是一個coroutine,所以線程不會等待asyncio.sleep(),而是直接中斷并執行下一個消息循環。當asyncio.sleep()返回時,線程就可以從yield from拿到返回值(此處是None),然后接著執行下一行語句。把asyncio.sleep(1)看成是一個耗時1秒的IO操作,在此期間主線程并未等待,而是去執行EventLoop中其他可以執行的coroutine了,因此可以實現并發執行。
asyncio + async/await
為了簡化并更好地標識異步IO,從Python3.5開始引入了新的語法async和await,可以讓coroutine的代碼更簡潔易讀。請注意,async和await是coroutine的新語法,使用新語法只需要做兩步簡單的替換:
把@asyncio.coroutine替換為async
把yield from替換為await
看如下代碼(在Python3.5以上版本使用):
#-*- coding:utf8 -*- import asyncio async def test(i): print('test_1', i) await asyncio.sleep(1) print('test_2', i) if __name__ == '__main__': loop = asyncio.get_event_loop() tasks = [test(i) for i in range(3)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
運行結果與之前一致。與前一節相比,這里只是把yield from換成了await,@asyncio.coroutine換成了async,其余不變。
Gevent
Gevent是一個基于Greenlet實現的網絡庫,通過greenlet實現協程。基本思想是一個greenlet就認為是一個協程,當一個greenlet遇到IO操作的時候,比如訪問網絡,就會自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由于IO操作非常耗時,經常使程序處于等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在運行,而不是等待IO操作。
Greenlet是作為一個C擴展模塊,它封裝了libevent事件循環的API,可以讓開發者在不改變編程習慣的同時,用同步的方式寫異步IO的代碼。
#-*- coding:utf8 -*- import gevent def test(n): for i in range(n): print(gevent.getcurrent(), i) if __name__ == '__main__': g1 = gevent.spawn(test, 3) g2 = gevent.spawn(test, 3) g3 = gevent.spawn(test, 3) g1.join() g2.join() g3.join()
運行結果:
<Greenlet at 0x10a6eea60: test(3)> 0 <Greenlet at 0x10a6eea60: test(3)> 1 <Greenlet at 0x10a6eea60: test(3)> 2 <Greenlet at 0x10a6eed58: test(3)> 0 <Greenlet at 0x10a6eed58: test(3)> 1 <Greenlet at 0x10a6eed58: test(3)> 2 <Greenlet at 0x10a6eedf0: test(3)> 0 <Greenlet at 0x10a6eedf0: test(3)> 1 <Greenlet at 0x10a6eedf0: test(3)> 2
復制代碼可以看到3個greenlet是依次運行而不是交替運行。要讓greenlet交替運行,可以通過gevent.sleep()交出控制權:
def test(n): for i in range(n): print(gevent.getcurrent(), i) gevent.sleep(1)
運行結果:
<Greenlet at 0x10382da60: test(3)> 0 <Greenlet at 0x10382dd58: test(3)> 0 <Greenlet at 0x10382ddf0: test(3)> 0 <Greenlet at 0x10382da60: test(3)> 1 <Greenlet at 0x10382dd58: test(3)> 1 <Greenlet at 0x10382ddf0: test(3)> 1 <Greenlet at 0x10382da60: test(3)> 2 <Greenlet at 0x10382dd58: test(3)> 2 <Greenlet at 0x10382ddf0: test(3)> 2
當然在實際的代碼里,我們不會用gevent.sleep()去切換協程,而是在執行到IO操作時gevent會自動完成,所以gevent需要將Python自帶的一些標準庫的運行方式由阻塞式調用變為協作式運行。這一過程在啟動時通過monkey patch完成:
#-*- coding:utf8 -*- from gevent import monkey; monkey.patch_all() from urllib import request import gevent def test(url): print('Get: %s' % url) response = request.urlopen(url) content = response.read().decode('utf8') print('%d bytes received from %s.' % (len(content), url)) if __name__ == '__main__': gevent.joinall([ gevent.spawn(test, 'http://httpbin.org/ip'), gevent.spawn(test, 'http://httpbin.org/uuid'), gevent.spawn(test, 'http://httpbin.org/user-agent') ])
運行結果:
Get: http://httpbin.org/ip Get: http://httpbin.org/uuid Get: http://httpbin.org/user-agent 53 bytes received from http://httpbin.org/uuid. 40 bytes received from http://httpbin.org/user-agent. 31 bytes received from http://httpbin.org/ip.
從結果看,3個網絡操作是并發執行的,而且結束順序不同,但只有一個線程。
總結
至此Python中的協程就介紹完畢了,示例程序中都是以sleep代表異步IO的,在實際項目中可以使用協程異步的讀寫網絡、讀寫文件、渲染界面等,而在等待協程完成的同時,CPU還可以進行其他的計算,協程的作用正在于此。那么協程和多線程的差異在哪里呢?多線程的切換需要靠操作系統來完成,當線程越來越多時切換的成本會很高,而協程是在一個線程內切換的,切換過程由我們自己控制,因此開銷小很多,這就是協程和多線程的根本差異。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。