Python與協程

什麼是協程

協程(coroutine),又稱微線程,纖程,是一種用戶級的輕量級線程。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。因此協程能保留上一次調用時的狀態,每次過程重入時,就相當於進入上一次調用的狀態。在併發編程中,協程與線程類似,每個協程表示一個執行單元,有自己的本地數據,與其他協程共享全局數據和其他資源。

協程需要用戶自己來編寫調度邏輯,對於CPU來說,協程其實是單線程,所以CPU不用去考慮怎麼調度、切換上下文,這就省去了CPU的切換開銷,所以協程在一定程度上又好於多線程。那麼在Python中是如何實現協程的呢?

yield關鍵字

Python通過yield關鍵字提供了對協程基本的支持,下面我們通過一段簡短的代碼來講解協程的生成器的基本行爲:

# 協程使用生成器函數定義: 定義體中有yield關鍵
def coroutine():
	print ("-> coroutine started")
	# 執行到這一步,協程暫停,通過x來接收住函數調用體傳來得數據
	x = yield
	print ("->coroutine received:", x)

if __name__ == "__main__":
	#生成生成器
	coro = coroutine()
	#激活協程
	next(coro)
	# 輸出: -> coroutine started
	# 協程通過函數調用send方法再次的調用
	# 調用send後,協程定義體中的yield表達式會計算出33,並將值賦值給x
	coro.send(33)
	#輸出: ->coroutine received:33 ,
	#協程結束
	coro.close()

當然yield關鍵字,我們還能通過inspect.getgeneratorstate(coro)函數獲取當前協程的狀態。結合以上的代碼,如:

if __name__ == "__main__":
	from inspect import getgeneratorstate
	coro = coroutine()
	print "--->>>",getgeneratorstate(coro)
	next(coro)
	print  "--->>>",getgeneratorstate(coro)
	coro.send(33)
	coro.close()
	print  "--->>>",getgeneratorstate(coro)

運行結果爲:

--->>>GEN_CREATED
--->>>GEN_SUSPENDED
--->>>GEN_CLOSED

gevent庫

儘管Python提供了yield關鍵字來提供了對協程的基本支持,但是不完全,在訪問網絡的IO操作時,出現阻塞時,好像並不是很友好,而使用第三方庫gevent庫應該是更好的選擇,gevent提供了比較完善的協程支持。

gevent是一個基於協程的Python網絡函數庫,使用greenlet在libev事件循環頂部提供了一個有高級別併發性的API。主要特性有以下幾點:

  • ·基於libev的快速事件循環,Linux上是epoll機制。
  • ·基於greenlet的輕量級執行單元。
  • ·API複用了Python標準庫裏的內容。
  • ·支持SSL的協作式sockets。
  • ·可通過線程池或c-ares實現DNS查詢。
  • ·通過monkey patching功能使得第三方模塊變成協作式。

gevent對協程的支持,本質上是greenlet在實現切換工作。greenlet工作流程如下:假如進行訪問網絡的IO操作時,出現阻塞,greenlet就顯式切換到另一段沒有被阻塞的代碼段執行,直到原先的阻塞狀況消失以後,再自動切換回原來的代碼段繼續處理。因此,greenlet是一種合理安排的串行方式。

由於IO操作非常耗時,經常使程序處於等待狀態,有了gevent爲我們自動切換協程,就保證總有greenlet在運行,而不是等待IO,這就是協程一般比多線程效率高的原因。由於切換是在IO操作時自動完成,所以gevent需要修改Python自帶的一些標準庫,將一些常見的阻塞,如socket、select等地方實現協程跳轉,這一過程在啓動時通過monkey patch完成。下面通過一個的例子來演示gevent的使用流程,代碼如下:

     from gevent import monkey; monkey.patch_all()
     import gevent
     import urllib2
     
     def run_task(url):
        print 'Visit --> %s' % url
        try:
                response = urllib2.urlopen(url)
                data = response.read()
                print '%d bytes received from %s.' % (len(data), url)
        except Exception,e:
                print e
     if __name__=='__main__':
        urls = ['https:// github.com/','https:// www.python.org/','http://www.cnblogs.com/']
        greenlets = [gevent.spawn(run_task, url) for url in urls  ]
        gevent.joinall(greenlets)

運行結果如下:

     Visit --> https:// github.com/
     Visit --> https:// www.python.org/
     Visit --> http://www.cnblogs.com/
     45740 bytes received from http://www.cnblogs.com/.
     25482 bytes received from https:// github.com/.
     47445 bytes received from https:// www.python.org/.

以上程序主要用了gevent中的spawn方法和joinall方法。spawn方法可以看做是用來形成協程,joinall方法就是添加這些協程任務,並且啓動運行。從運行結果來看,3個網絡操作是併發執行的,而且結束順序不同,但其實只有一個線程。

gevent中還提供了對池的支持。當擁有動態數量的greenlet需要進行併發管理(限制併發數)時,就可以使用池,這在處理大量的網絡和IO操作時是非常需要的。接下來使用gevent中pool對象,對上面的例子進行改寫,程序如下:

     from gevent import monkey
     monkey.patch_all()
     import urllib2
     from gevent.pool import Pool
     def run_task(url):
        print 'Visit --> %s' % url
        try:
                response = urllib2.urlopen(url)
                data = response.read()
                print '%d bytes received from %s.' % (len(data), url)
        except Exception,e:
                print e
        return 'url:%s --->finish'% url
     
     if __name__=='__main__':
        pool = Pool(2)
        urls = ['https:// github.com/','https:// www.python.org/','http://www.cnblogs.com/']
        results = pool.map(run_task,urls)
        print results

運行結果如下:

     Visit --> https:// github.com/
     Visit --> https:// www.python.org/
     25482 bytes received from https:// github.com/.
     Visit --> http://www.cnblogs.com/
     47445 bytes received from https:// www.python.org/.
     45687 bytes received from http://www.cnblogs.com/.
['url:https:// github.com/ --->finish', 'url:https:// www.python.org/ --->finish', 'url:http://www.cnblogs.com/   --->finish']

通過運行結果可以看出,Pool對象確實對協程的併發數量進行了管理,先訪問了前兩個網址,當其中一個任務完成時,纔會執行第三個。

參考書籍: 1、《流暢的Python》 2、《Python爬蟲開發與項目實戰》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章