如何讓 python 處理速度翻倍?內含代碼

做爲在平常開發生產中很是實用的語言,有必要掌握一些python用法,好比爬蟲、網絡請求等場景,非常實用。但python是單線程的,如何提升python的處理速度,是一個很重要的問題,這個問題的一個關鍵技術,叫協程。本篇文章,講講python協程的理解與使用,主要是針對網絡請求這個模塊作一個梳理,但願能幫到有須要的同窗。python

概念篇面試

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

 進程
在面試的時候,咱們都會記住一個概念,進程是系統資源分配的最小單位。是的,系統由一個個程序,也就是進程組成的,通常狀況下,分爲文本區域、數據區域和堆棧區域。
文本區域存儲處理器執行的代碼(機器碼),一般來講,這是一個只讀區域,防止運行的程序被意外修改。
數據區域存儲全部的變量和動態分配的內存,又細分爲初始化的數據區(全部初始化的全局、靜態、常量,以及外部變量)和爲初始化的數據區(初始化爲0的全局變量和靜態變量),初始化的變量最初保存在文本區,程序啓動後被拷貝到初始化的數據區。
堆棧區域存儲着活動過程調用的指令和本地變量,在地址空間裏,棧區緊連着堆區,他們的增加方向相反,內存是線性的,因此咱們代碼放在低地址的地方,由低向高增加,棧區大小不可預測,隨開隨用,所以放在高地址的地方,由高向低增加。當堆和棧指針重合的時候,意味着內存耗盡,形成內存溢出。
進程的建立和銷燬都是相對於系統資源,很是消耗資源,是一種比較昂貴的操做。進程爲了自身能獲得運行,必需要搶佔式的爭奪CPU。對於單核CPU來講,在同一時間只能執行一個進程的代碼,因此在單核CPU上實現多進程,是經過CPU快速的切換不一樣進程,看上去就像是多個進程在同時進行。
因爲進程間是隔離的,各自擁有本身的內存內存資源,相比於線程的共同共享內存來講,相對安全,不一樣進程之間的數據只能經過 IPC(Inter-Process Communication) 進行通訊共享。 json

線程
線程是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的協程。
場景
外部接收一些文件,每一個文件裏有一組數據,其中,這組數據須要經過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的閥值。能夠看到,優化的程度很顯著。

相關文章
相關標籤/搜索