機器學習之Python基礎(五) --協程,分散式

這篇文章講述python關於協程和分散式進程的相關知識,標題如下

  1. 協程概念簡述
  2. 協程之yield
  3. 協程之gevent
  4. 協程之asnycio
  5. 分散式進程之分散式系統
  6. 分散式進程之managers

協程概念簡述

協程(Coroutine)又稱微程,纖程。

子程序(又名「函數」)在所有語言中都是「層級調用」,換言之,就是A調用B,B調用C,那麼就要等待C執行完成後返回到B,B完成後返回到A,然後A執行完成。正如我們所知的,這種運行模式是通過棧來實現的。

協程看上去也像是子程序,但是在執行過程中,協程在子程序的內部可以中斷,然後轉而執行別的子程序,在某個恰當的時間點又轉回來執行這個子程序。

協程看起來更像是多線程模式的運行,但實際上它只用了一個線程,因此相比多線程,協程擁有極高的運行效率,因為協程不需要由CPU調度切換,而由程序自己控制;並且協程還不需要使用多線程的鎖機制,因為只有一個線程就不存在變數衝突,在協程中控制共享資源只需要判斷狀態即可,因此也沒有死鎖的風險。

如果我們要利用多核CPU,就可以通過多進程+協程的方式來進行,既充分地了利用了多核,又充分地發揮了協程地高效率,可以獲得極高地性能。

協程之yield

Python用於generator的yield可以一定程度上實現協程的功效。我們來看下面這個例子:

假設我們需要同時執行兩個任務,一個任務用於生成一個數據,一個任務用於接收這個數據並列印出來,那麼如果用多線程模式進行編程的話,我們就需要兩個線程,一個線程寫數據一個線程拿數據,並且通過鎖機制和隊列控制等待。如果用yield的話,則可以這麼做

這段代碼與我們之前常用的寫法似乎有些區別,因為我們看到子程序outputter壓根沒有跟在return後面的返回值,但是不論是在主程序中,還是在子程序producer中,都接收了outputter的返回值並且二者接收的內容顯然還是不一樣的。

要理解這種情況,我們首先要明白yield是如何返回一個生成器(generator)的。

我們知道,代碼for ... in ...可以將in後面的對象的內容迭代出來,這個過程是可以重複的,簡單的說,像是執行了代碼for x in

listA一次,後面我們依然可以用for...in...語句來迭代listA,listA依然能夠被迭代,因為列表listA是一個可迭代對象。

但是,如果我們有大量的數據要迭代,並且不希望把它們存在一個類似於列表這樣的可迭代對象裡面,因為這樣會很占內存,那麼我們就可以考慮使用生成器來完成這個工作。生成器也是一種迭代器,但它只能被迭代一次,因為它實時地生成這些值,而這些值一旦被迭代後就會被銷毀,不再是先都存放內存當中然後一個一個迭代了。我們可以參考下面這個常式

我們可以看到,儘管我們運行了兩次for...in...語句,但成功的只有第一次,因為a就是一個簡單的生成器,它只能被迭代一次。(如果列印a,我們就會發現a是一個生成器。)

yield就是這麼一個生成generator的關鍵詞,它類似於return,在一個子程序中調用yield的時候,這個子程序的第一次運行僅會運行到有yield的位置然後直接結束

我們可以看到,這就是為什麼主程序中變數b接收到的內容是一個生成器。而每當我們調用一次函數next()(也可以這樣調用next(b),這個函數在generator對象內是通過實例方法__next__來編寫的)時,就會執行一次生成器函數test_func內的循環,又因為我們添加的條件語句,使得需要n內有值才會繼續運行,否則會return,因此我們還需要通過send()方法往生成器裡面傳輸內容。

到此,我們就簡單地了解到了yield實現協程工作方式的原理。接下來為大家介紹Python實現協程的兩個模塊geventasyncio。

協程之gevent

但是yield在Python中算式比較低級的對協程模式的支持,它還有很多不完善的內容。第三方模塊gevent就是一個提供了比較完善的協程支持的模塊。

gevent通過greenlet實現協程:

當一個greenlet遇到IO操作時,它會自動切換到其他的greenlet執行任務,等到IO操作完成時又在適當的時候切換回來運行。我們直到IO操作通常比較耗時,因此有這種自動切換機制就可以讓我們的任務在等待IO操作時可以「順便」做點兒其他的事。

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

如果這樣子使用的話,3個greenlet會依次運行。

要讓greenlet交替運行,可以通過gevent.sleep()交出控制權:

這樣做就可以實現交替運行了。

不過實際上我們在使用gevent的時候是當然不會去用gevent.sleep()去切換協程的,因為greenlet在執行到IO操作時,是會自動切換的:

可以看到,greenlet「start」後在IO操作期間會切換到其它greenlet運行。另外要注意,這裡monkey調用的方法是monkey.patch_all(),和上一個常式不一樣。

協程之asnycio

asyncio是Python 3.4版本引入的標準庫,直接內置了對非同步IO的支持。

asyncio的編程模型就是一個消息循環。我們從asyncio模塊中直接獲取一個EventLoop的引用,然後把需要執行的協程扔到EventLoop中執行,就實現了非同步IO。以下是例子

@asyncio.coroutine 裝飾器把一個generator標記為coroutine類型,然後把coroutine放在事件循環loop中執行。在上面的程序中,程序執行到yeild from,由於asyncio.sleep()也是一個協程,所以線程不會等待asyncio.sleep(),而是直接中斷並執行下一個消息循環。因此你會看到程序先列印了兩個holle world。等待了大約1秒後asyncio.sleep()(這裡我們可以把asyncio.sleep(1)看作要耗時1秒的IO操作)返回時,線程就可以從yield from拿到返回值(此處是None),然後接著執行下一行語句。最後列印了了兩個holle again,並且由列印信息我們可以發現以上協程均在同一個線程上並發執行。

在Python3.5後,引入了新的關鍵字asynsawaitasynsawait分別與@asyncio.coroutineyield from 等價,使得協程的代碼更簡潔易讀。所以上面的協程holle()可以寫為

分散式進程之分散式系統

我們知道,相比多線程,多進程的效果往往要更好,但是如果任務數量實在龐大,在一台機器上進行有限的多進程工作模式也往往很難吃的消。這個時候,我們就需要把多進程分配到多台機器上通過網路互相通信進行協同工作,相比最多只能分布到多個CPU的線程,這種工作模式的效率會有非常大的提升。

這種建立在網路之上的軟體系統,就稱為分散式系統(distributed system

分散式系統有兩大特點:內聚性和透明性

  • 內聚性:每一個資料庫節點高度自治,有本地的資料庫管理系統。
  • 透明性:每一個資料庫分布節點對於用戶的應用來說都是透明的,是無法區分本地還是遠程的。

現在我們來了解一下,如何使用Python搭建分散式進程工作模式。

分散式進程之managers

Python中multiprocessing的子模塊managers支持把多進程分布到多台機器上,一個服務進程可以作為調度者來將任務分布到其它的多個進程當中,並依靠網路進行互相通信。由於managers的模塊封裝好了,所以在Python中我們調用它時可以不需要了解網路通信的底層細節,就可以直接進行分散式多進程程序的編寫:

我們現在假設我們有3個數據要傳到另一台機器上,希望另一台機器將這3個數據進行加密(例如把它們都加上10)後返回給原來的這台機器,我們可以通過傳輸隊列對象Queue來進行這個任務的實現:

這是作為第一台機器調度加密任務並接受加密後數據信息的常式manager.py

上面是子進程運行代碼worker.py

我們先啟動manager.py,可以看到

說明主進程開始運行,並且在等待結果。然後我們運行子進程代碼worker.py

成功連接到主進程,並獲取了數據進行處理,然後傳輸回主進程

主進程得到加密後的數據結果,並將其列印出來。

這便是一個簡單的分散式運算,其中,Queue對象就存儲在主進程manager.py中,worker.py沒有創建Queue對象而是直接對manager.py中的Queue對象做出修改。

本文首發於公眾號「AI遇見機器學習」,更多乾貨可搜索[mltoai]或直接公眾號名,歡迎關注!

推薦閱讀:

幾個提高工作效率的Python內置小工具
高德API+Python解決租房問題
爬蟲入門系列(一):快速理解 HTTP 協議
數據可視化--Matplotlib
python 函數

TAG:Python | 机器学习 |