天天看點

如何讓 python 處理速度翻倍?内含代碼概念篇原理篇實戰篇

作者 | 墨辨

概念篇

在了解協程這個概念及其作用場景前,先要了解幾個基本的關于作業系統的概念,主要是程序、線程、同步、異步、阻塞、非阻塞,了解這幾個概念,不僅是對協程這個場景,諸如消息隊列、緩存等,都有一定的幫助。接下來,編者就自己的了解和網上查詢的材料,做一個總結。

程序

在面試的時候,我們都會記住一個概念,程序是系統資源配置設定的最小機關。是的,系統由一個個程式,也就是程序組成的,一般情況下,分為文本區域、資料區域和堆棧區域。

文本區域存儲處理器執行的代碼(機器碼),通常來說,這是一個隻讀區域,防止運作的程式被意外修改。

資料區域存儲所有的變量和動态配置設定的記憶體,又細分為初始化的資料區(所有初始化的全局、靜态、常量,以及外部變量)和為初始化的資料區(初始化為0的全局變量和靜态變量),初始化的變量最初儲存在文本區,程式啟動後被拷貝到初始化的資料區。

堆棧區域存儲着活動過程調用的指令和本地變量,在位址空間裡,棧區緊連着堆區,他們的增長方向相反,記憶體是線性的,是以我們代碼放在低位址的地方,由低向高增長,棧區大小不可預測,随開随用,是以放在高位址的地方,由高向低增長。當堆和棧指針重合的時候,意味着記憶體耗盡,造成記憶體溢出。

程序的建立和銷毀都是相對于系統資源,非常消耗資源,是一種比較昂貴的操作。程序為了自身能得到運作,必須要搶占式的争奪CPU。對于單核CPU來說,在同一時間隻能執行一個程序的代碼,是以在單核CPU上實作多程序,是通過CPU快速的切換不同程序,看上去就像是多個程序在同時進行。

由于程序間是隔離的,各自擁有自己的記憶體記憶體資源,相比于線程的共同共享記憶體來說,相對安全,不同程序之間的資料隻能通過 IPC(Inter-Process Communication) 進行通信共享。

線程

線程是CPU排程的最小機關。如果程序是一個容器,線程就是運作在容器裡面的程式,線程是屬于程序的,同個程序的多個線程共享程序的記憶體位址空間。

線程間的通信可以直接通過全局變量進行通信,是以相對來說,線程間通信是不太安全的,是以引入了各種鎖的場景,不在這裡闡述。

當一個線程崩潰了,會導緻整個程序也崩潰了,即其他線程也挂了, 但多程序而不會,一個程序挂了,另一個程序依然照樣運作。

在多核作業系統中,預設程序内隻有一個線程,是以對多程序的處理就像是一個程序一個核心。

同步和異步

同步和異步關注的是消息通信機制,所謂同步,就是在發出一個函數調用時,在沒有得到結果之前,該調用不會傳回。一旦調用傳回,就立即得到執行的傳回值,即調用者主動等待調用結果。所謂異步,就是在請求發出去後,這個調用就立即傳回,沒有傳回結果,通過回調等方式告知該調用的實際結果。

同步的請求,需要主動讀寫資料,并且等待結果;異步的請求,調用者不會立刻得到結果。而是在調用發出後,被調用者通過狀态、通知來通知調用者,或通過回調函數處理這個調用。

阻塞和非阻塞

阻塞和非阻塞關注的是程式在等待調用結果(消息,傳回值)時的狀态。

阻塞調用是指調用結果傳回之前,目前線程會被挂起。調用線程隻有在得到結果之後才會傳回。非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞目前線程。是以,區分的條件在于,程序/線程要通路的資料是否就緒,程序/線程是否需要等待。

非阻塞一般通過多路複用實作,多路複用有 select、poll、epoll幾種實作方式。

協程

在了解前面的幾個概念後,我們再來看協程的概念。

協程是屬于線程的,又稱微線程,纖程,英文名Coroutine。舉個例子,在執行函數A時,我希望随時中斷去執行函數B,然後中斷B的執行,切換回來執行A。這就是協程的作用,由調用者自由切換。這個切換過程并不是等同于函數調用,因為它沒有調用語句。執行方式與多線程類似,但是協程隻有一個線程執行。

協程的優點是執行效率非常高,因為協程的切換由程式自身控制,不需要切換線程,即沒有切換線程的開銷。同時,由于隻有一個線程,不存在沖突問題,不需要依賴鎖(加鎖與釋放鎖存在很多資源消耗)。

協程主要的使用場景在于處理IO密集型程式,解決效率問題,不适用于CPU密集型程式的處理。然而實際場景中這兩種場景非常多,如果要充分發揮CPU使用率,可以結合多程序+協程的方式。後續我們會講到結合點。

原理篇

根據wikipedia的定義,協程是一個無優先級的子程式排程元件,允許子程式在特點的地方挂起恢複。是以理論上,隻要記憶體足夠,一個線程中可以有任意多個協程,但同一時刻隻能有一個協程在運作,多個協程分享該線程配置設定到的計算機資源。協程是為了充分發揮異步調用的優勢,異步操作則是為了避免IO操作阻塞線程。

知識準備

在了解原理前,我們先做一個知識的準備工作。

1)現代主流的作業系統幾乎都是分時作業系統,即一台計算機采用時間片輪轉的方式為多個使用者服務,系統資源配置設定的基本機關是程序,CPU排程的基本機關是線程。

2)運作時記憶體空間分為變量區,棧區,堆區。記憶體位址配置設定上,堆區從低地到高,棧區從高往低。

3)計算機執行時一條條指令讀取執行,執行到目前指令時,下一條指令的位址在指令寄存器的IP中,ESP寄存值指向目前棧頂位址,EBP指向目前活動棧幀的基位址。

4)系統發生函數調用時操作為:先将入參從右往左依次壓棧,然後把傳回位址壓棧,最後将目前EBP寄存器的值壓棧,修改ESP寄存器的值,在棧區配置設定目前函數局部變量所需的空間。

5)協程的上下文包含屬于目前協程的棧區和寄存器裡面存放的值。

**事件循環

**

在python3.3中,通過關鍵字yield from使用協程,在3.5中,引入了關于協程的文法糖async和await,我們主要看async/await的原了解析。其中,事件循環是一個核心所在,編寫過 js的同學,會對事件循環Eventloop更加了解, 事件循環是一種等待程式配置設定事件或消息的程式設計架構(維基百科)。在python中,asyncio.coroutine 修飾器用來标記作為協程的函數, 這裡的協程是和asyncio及其事件循環一起使用的,而在後續的發展中,async/await被使用的越來越廣泛。

async/await

async/await是使用python協程的關鍵,從結構上來看,asyncio 實質上是一個異步架構,async/await 是為異步架構提供的 API已友善使用者調用,是以使用者要想使用async/await 編寫協程代碼,目前必須機遇 asyncio 或其他異步庫。

Future

在實際開發編寫異步代碼時,為了避免太多的回調方法導緻的回調地獄,但又需要擷取異步調用的傳回結果結果,聰明的語言設計者設計了一個 叫Future的對象,封裝了與loop 的互動行為。其大緻執行過程為:程式啟動後,通過add_done_callback 方法向 epoll 注冊回調函數,當 result 屬性得到傳回值後,主動運作之前注冊的回調函數,向上傳遞給 coroutine。這個Future對象為asyncio.Future。

但是,要想取得傳回值,程式必須恢複恢複工作狀态,而由于Future 對象本身的生存周期比較短,每一次注冊回調、産生事件、觸發回調過程後工作可能已經完成,是以用 Future 向生成器 send result 并不合适。是以這裡又引入一個新的對象 Task,儲存在Future 對象中,對生成器協程進行狀态管理。

Python 裡另一個 Future 對象是 concurrent.futures.Future,與 asyncio.Future 互不相容,容易産生混淆。差別點在于,concurrent.futures 是線程級的 Future 對象,當使用 concurrent.futures.Executor 進行多線程程式設計時,該對象用于在不同的 thread 之間傳遞結果。

Task

上文中提到,Task是維護生成器協程狀态處理執行邏輯的的任務對象,Task 中有一個_step 方法,負責生成器協程與 EventLoop 互動過程的狀态遷移,整個過程可以了解為:Task向協程 send 一個值,恢複其工作狀态。當協程運作到斷點後,得到新的Future對象,再處理 future 與 loop 的回調注冊過程。

Loop

在日常開發中,會有一個誤區,認為每個線程都可以有一個獨立的 loop。實際運作時,主線程才能通過 asyncio.get_event_loop() 建立一個新的 loop,而在其他線程時,使用 get_event_loop() 卻會抛錯。正确的做法為通過 asyncio.set_event_loop() ,将目前線程與 主線程的loop 顯式綁定。

Loop有一個很大的缺陷,就是 loop 的運作狀态不受 Python 代碼控制,是以在業務進行中,無法穩定的将協程拓展到多線程中運作。

總結

如何讓 python 處理速度翻倍?内含代碼概念篇原理篇實戰篇

實戰篇

介紹完概念和原理,我來看看如何使用,這裡,舉一個實際場景的例子,來看看如何使用python的協程。

場景

外部接收一些檔案,每個檔案裡有一組資料,其中,這組資料需要通過http的方式,發向第三方平台,并獲得結果。

分析

由于同一個檔案的每一組資料沒有前後的處理邏輯,在之前通過Requests庫發送的網絡請求,串行執行,下一組資料的發送需要等待上一組資料的傳回,顯得整個檔案的處理時間長,這種請求方式,完全可以由協程來實作。

為了更友善的配合協程發請求,我們使用aiohttp庫來代替requests庫,關于aiohttp,這裡不做過多剖析,僅做下簡單介紹。

aiohttp

aiohttp是asyncio和Python的異步HTTP用戶端/伺服器,由于是異步的,經常用在服務區端接收請求,和用戶端爬蟲應用,發起異步請求,這裡我們主要用來發請求。

aiohttp支援用戶端和HTTP伺服器,可以實作單線程并發IO操作,無需使用Callback Hell即可支援Server WebSockets和Client WebSockets,且具有中間件。

代碼實作

直接上代碼了,talk is cheap, show me the code~

import aiohttp
import asyncio
from inspect import isfunction
import time
import logger

@logging_utils.exception(logger)
def request(pool, data_list):
    loop = asyncio.get_event_loop()
    loop.run_until_complete(exec(pool, data_list))


async def exec(pool, data_list):
    tasks = []
    sem = asyncio.Semaphore(pool)
    for item in data_list:
        tasks.append(
            control_sem(sem,
                        item.get("method", "GET"),
                        item.get("url"),
                        item.get("data"),
                        item.get("headers"),
                        item.get("callback")))
    await asyncio.wait(tasks)


async def control_sem(sem, method, url, data, headers, callback):
    async with sem:
        count = 0
        flag = False
        while not flag and count < 4:
            flag = await fetch(method, url, data, headers, callback)
            count = count + 1
            print("flag:{},count:{}".format(flag, count))
        if count == 4 and not flag:
            raise Exception('EAS service not responding after 4 times of retry.')


async def fetch(method, url, data, headers, callback):
    async with aiohttp.request(method, url=url, data=data, headers=headers) as resp:
        try:
            json = await resp.read()
            print(json)
            if resp.status != 200:
                return False
            if isfunction(callback):
                callback(json)
            return True
        except Exception as e:
            print(e)
           

這裡,我們封裝了對外發送批量請求的request方法,接收一次性發送的資料多少,和資料綜合,在外部使用時,隻需要建構好網絡請求對象的資料,設定好請求池大小即可,同時,設定了重試功能,進行了4次重試,防止在網絡抖動的時候,單個資料的網絡請求發送失敗。

最終效果

在使用協程重構網絡請求子產品之後,當資料量在1000的時候,由之前的816s,提升到424s,快了一倍,且請求池大小加大的時候,效果更明顯,由于第三方平台同時建立連接配接的資料限制,我們設定了40的閥值。可以看到,優化的程度很顯著。

編者說

人生苦短,我用python。協程好不好,誰用誰知道。如果有類似的場景,可以考慮啟用,或者其他場景,歡迎留言讨論。

參考資料:

了解async/await:

https://segmentfault.com/a/1190000015488033?spm=ata.13261165.0.0.57d41b119Uyp8t

協程概念,原理(c++和node.js實作)

https://cnodejs.org/topic/58ddd7a303d476b42d34c911?spm=ata.13261165.0.0.57d41b119Uyp8t