提升python處理速度原理及方法實例
這篇文章主要介紹了提升python處理速度原理及方法實例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
導(dǎo)讀:作為日常生產(chǎn)開發(fā)中非常實用的一門語言,python廣泛應(yīng)用于網(wǎng)絡(luò)爬蟲、web開發(fā)、自動化測試、數(shù)據(jù)分析和人工智能等領(lǐng)域。但python是單線程的,想要提升python的處理速度,涉及到一個很關(guān)鍵的技術(shù)——協(xié)程。本篇文章,將講述python協(xié)程的理解與使用。
1、操作系統(tǒng)相關(guān)概念
在理解與使用協(xié)程之前,先簡單的了解幾個與操作系統(tǒng)相關(guān)的概念,包括進程、線程、同步和異步、阻塞與非阻塞。了解這些概念,對你學(xué)習(xí)協(xié)程、消息隊列、緩存等知識都有一定的幫助。
(1)進程:
進程是操作系統(tǒng)分配資源的最小單位,系統(tǒng)由一個個程序(進程)組成的,一般而言,分為文本區(qū)域、數(shù)據(jù)區(qū)域和堆棧區(qū)域
文本區(qū)域存儲處理器執(zhí)行的代碼(機器碼),通常來說,這是一個只讀區(qū)域,防止運行的程序被意外的修改
數(shù)據(jù)區(qū)域存儲所有的變量和動態(tài)分配的內(nèi)存,又細分為初始化的數(shù)據(jù)區(qū)(所有初始化的全局、靜態(tài)、常量以及外部變量)和未初始化的數(shù)據(jù)區(qū)(初始化未0的全局變量和靜態(tài)變量),初始化的變量最初保存在文本區(qū),程序啟動后被拷貝到初始化的數(shù)據(jù)區(qū)
堆棧區(qū)域存儲著活動過程調(diào)用的指令和本地變量,在地址空間里,棧區(qū)緊連著堆區(qū),他們的增長方向相反,內(nèi)存是線性的,所以我們的代碼放在低地址的地方,由低向高增長,棧區(qū)大小不可預(yù)測,隨開隨用,因此放在高地址的地方,由高向低增長。當(dāng)堆與棧指針重合的時候,意味著內(nèi)存耗盡,造成內(nèi)存溢出。
進程的創(chuàng)建和銷毀都非常的消耗系統(tǒng)資源,是一種比較昂貴的操作。進程為了自身能夠得到運行,必須搶占式的爭奪CPU。對于單核CPU而言,在同一時間內(nèi)只能執(zhí)行一個進程的代碼,所以在單核CPU上實現(xiàn)多進程,是通過CPU的快速切換不同進程來實現(xiàn)的,看上去就像是多個進程同時執(zhí)行。
由于進程間是隔離的,各自擁有自己的內(nèi)存資源,相比于線程的共享內(nèi)存而言,要更安全,不同進程之間的數(shù)據(jù)只能通過IPC(Inter-Process Communication)進行通信共享
(2)線程
線程是CPU調(diào)度的基本單位。如果進程是一個容器,線程就是運行在容器里面的程序,線程是屬于進程的,同個進程的多個線程共享進程的內(nèi)存地址空間
線程間可以直接通過全局變量進行通信,所以相對來說,線程間通信是不太安全的,因此引入各種鎖的場景,這里將不闡述
當(dāng)一個線程奔潰了,會導(dǎo)致整個進程也奔潰,即其它線程也掛了。這一點與進程不一樣,一個進程掛了,其他進程照樣執(zhí)行
在多核操作系統(tǒng)中,默認一個進程內(nèi)只有一個線程,所以對多進程處理就像是一個進程一個核心
(3)同步和異步
同步和異步關(guān)注的是消息通信機制,所謂同步,就是在發(fā)出一個函數(shù)調(diào)用時,在沒有得到結(jié)果之前,該調(diào)用不會返回。一旦調(diào)用返回,就立即得到調(diào)用的返回值,即調(diào)用者主動等待調(diào)用結(jié)果
所謂異步,就是在請求發(fā)出去后,這個調(diào)用就立即返回,但沒有返回結(jié)果,通過回調(diào)的方式告知該調(diào)用的實際結(jié)果
同步的請求,需要主動讀寫數(shù)據(jù),并且等待結(jié)果;異步的請求,調(diào)用者不會立即得到結(jié)果。而是在調(diào)用發(fā)出后,被調(diào)用者通過狀態(tài)、通知來告訴調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用
(4)阻塞與非阻塞
阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時的狀態(tài)
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起。調(diào)用線程只有在得到結(jié)果之后才會返回
非阻塞調(diào)用指在得到不能立即得到結(jié)果之前,該調(diào)用不會阻塞當(dāng)前線程。所以,區(qū)分的條件在于,進程/線程要訪問的數(shù)據(jù)是否就緒,進程/線程是否需要等待
非阻塞一般通過多路復(fù)用實現(xiàn),多路復(fù)用由select、poll、epoll幾種實現(xiàn)方式
(5)協(xié)程
了解完前面幾個概念,再來看看協(xié)程的概念
協(xié)程是屬于線程的,又稱微線程,纖程,英文名是coroutine。舉個例子,在執(zhí)行函數(shù)A時,我希望能隨時終端去執(zhí)行函數(shù)B,然后終端B的執(zhí)行,切換回來執(zhí)行函數(shù)A。這就是協(xié)程的作用,由調(diào)用者自有切換。這個切換過程并不等同于函數(shù)調(diào)用,因為它沒有調(diào)用語句。執(zhí)行方式與多線程類似,但是協(xié)程只有一個線程執(zhí)行
協(xié)程的優(yōu)點是執(zhí)行效率非常高,因為協(xié)程的切換是由程序自身控制,不需要切換線程,即沒有切換線程的開銷。同時,由于只有一個線程,不存在沖突的問題,不需要依賴鎖(加鎖和釋放鎖需要很多資源消耗)
協(xié)程的主要使用場景在于處理io密集型程序,解決效率問題,不同于CPU密集型程序的處理。然而實際開發(fā)中這兩種場景非常多,如果要充分發(fā)揮CPU的利用率,可以使用多進程+協(xié)程的方式,本文后續(xù)將講到結(jié)合點
2、協(xié)程相關(guān)原理
根據(jù)wikipedia的定義,協(xié)程是一個無優(yōu)先級的子程序調(diào)度組件,允許子程序在特定的地方掛起恢復(fù)。所以理論上,只要內(nèi)存足夠,一個線程可以有任意多個協(xié)程,但同一時刻只能有一個協(xié)程在運行,多個協(xié)程分享該線程分配到的計算機資源。協(xié)程是為了充分發(fā)揮異步調(diào)用的優(yōu)勢,異步操作則是為了IO操作阻塞線程
(1)知識準備
在了解原理前,先做一個知識的準備
1)現(xiàn)代主流的操作系統(tǒng)幾乎都是分時操作系統(tǒng),即一臺計算機采用時間片輪轉(zhuǎn)的方式為多個用戶提供服務(wù),系統(tǒng)資源分配的基本單位是進程,CPU調(diào)度的基本單位是線程
2)運行時內(nèi)存空間氛圍變量區(qū)、棧區(qū)、堆區(qū)。內(nèi)存地址分配上,堆區(qū)從低到高,棧區(qū)從高到低
3)計算機執(zhí)行時一條條指令讀取執(zhí)行,執(zhí)行到當(dāng)前指令時,下一條指令的指令的地址在指令寄存器的IP中,ESP寄存值只想當(dāng)前棧頂?shù)刂?,EBP指向當(dāng)前活動棧幀的基地址
4)系統(tǒng)發(fā)生函數(shù)調(diào)用時操作為:先將入?yún)挠彝笠淮螇簵#缓蟀逊祷氐刂穳簵?,最后將?dāng)前EBP寄存器的值壓棧,修改ESP寄存器的值,在棧區(qū)分配當(dāng)前函數(shù)局部變量所需的空間
5)協(xié)程的上下文包含屬于當(dāng)前協(xié)程的棧區(qū)和寄存器里面存放的值
(2)事件循環(huán)
在python3.3中通過yield from使用協(xié)程,在3.5中,引入了關(guān)于協(xié)程的語法糖async/await的原理解析。其中,事件循環(huán)是一個核心所在,編寫過js的同學(xué),會對事件循環(huán)Eventloop更加了解,事件循環(huán)是一種等待程序分配消息或事件的編程架構(gòu)。在python中,asyncio.coroutine修飾器用來標記作為協(xié)程的函數(shù),這里的協(xié)程是和asyncio及其事件循環(huán)一起使用的,而在后續(xù)的發(fā)展中,async/await被使用的越來越廣泛
(3)async/await
async/await是使用python協(xié)程的關(guān)鍵,從結(jié)構(gòu)上來看,asyncio實質(zhì)上是一個異步框架,async/await是為異步框架提供API以方便使用者調(diào)用,所以使用者要想使用async/await編寫協(xié)程代碼,目前必須基于asyncio或其他異步庫
(4)Future
在實際開發(fā)編寫異步代碼時,為了避免太多回調(diào)方法導(dǎo)致的回調(diào)地獄,但又需要獲取異步調(diào)用的返回結(jié)果,聰明的語言設(shè)計者設(shè)計了一個叫做Future的對象,封裝了與loop的交互行為。其大致執(zhí)行過程為:程序啟動后,通過add_done_callback方法向epoll注冊回調(diào)函數(shù),當(dāng)result屬性得到返回值后,主動運行之前注冊的回調(diào)函數(shù),向上傳遞給coroutine。這個Future對象為asyncio.Future
但是,要想取得返回值,程序必須恢復(fù)到工作狀態(tài),而由于Future對象本身的生存周期比較短,每一次注冊回調(diào)、產(chǎn)生事件、觸發(fā)回調(diào)過程后工作可能已經(jīng)完成,所以用Future向生成器send result并不合適。這里又引入一個新的對象Task,保存在Future對象中,對生成器協(xié)程進行狀態(tài)管理
Python里另一個Future對象是concurrent.futures.Future,與asyncio.Future互不兼容,容易產(chǎn)生混淆。區(qū)別點在于,concurrent.futures是線程級的Future對象,當(dāng)使用concurrent.futures.Executor進行多線程編程時,該對象用于在不同的thread之間傳遞結(jié)果
(5)Task
上文中提到,Task是維護生成器協(xié)程狀態(tài)處理執(zhí)行邏輯的任務(wù)對象,Task中有一個_step方法,負責(zé)生成器協(xié)程與EventLoop交互過程的狀態(tài)遷移,整個過程可以理解為:Task向協(xié)程send一個值,恢復(fù)其工作狀態(tài)。當(dāng)協(xié)程運行到斷點后,得到新的Future對象,再處理future與loop的回調(diào)注冊過程
(6)Loop
在日常開發(fā)中,會有一個誤區(qū),認為每一個線程都可以有一個獨立的loop。實際運行時,主線程才能通過asyncio.get_event_loop()創(chuàng)建一個新的loop,而在其他線程時,使用get_event_loop()卻會拋錯。正確的做法為通過asyncio.set_event_loop(),將當(dāng)前線程與主線程loop顯式綁定
3、協(xié)程實戰(zhàn)
上面介紹完了協(xié)程相關(guān)的概念和原理,接下來看看如何使用,這里舉一個實際場景的例子
場景:
外部接受一些文件,每個文件里有一些數(shù)據(jù),其中,這組數(shù)據(jù)需要通過http的方式,發(fā)向第三方平臺,并獲得結(jié)果
分析:
由于同一文件的每一組數(shù)據(jù)沒有前后的處理邏輯,在之前通過requests庫發(fā)送的網(wǎng)絡(luò)請求,串行執(zhí)行,下一組數(shù)據(jù)的發(fā)送需要等待上一組數(shù)據(jù)的返回,顯得整個文件的處理時間長,這種請求方式,完全可以由協(xié)程來實現(xiàn)
為了更方便的配合協(xié)程發(fā)請求,我們使用aiohttp庫來代替requests庫,關(guān)于aiohttp,下面做簡單介紹
aiohttp:
aiohttp是asyncio和python的異步HTTP客戶端/服務(wù)器,由于是異步的,經(jīng)常用在服務(wù)器端接收請求,和客戶端爬蟲應(yīng)用,發(fā)起異步請求,這里我們主要用來發(fā)請求
aiohttp支持客戶端和HTTP服務(wù)器,可以實現(xiàn)單線程并發(fā)IO操作,無需使用Callback Hell即可支持Server WebSockets和Client WebSockets,且具有中間件
4、代碼實現(xiàn)
直接上代碼吧,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)
這里,我們封裝了對外發(fā)送批量請求的request方法,接收一次性發(fā)送的數(shù)據(jù)多少,和數(shù)據(jù)綜合,在外部使用時,只需要構(gòu)建好網(wǎng)絡(luò)請求對象的數(shù)據(jù),設(shè)定好請求池大小即可,同時,設(shè)置了重試功能,進行了4次重試,防治在網(wǎng)絡(luò)抖動的時候,單個數(shù)據(jù)的網(wǎng)絡(luò)請求發(fā)送失敗
最終效果:
在使用協(xié)程重構(gòu)網(wǎng)絡(luò)請求模塊之后,當(dāng)數(shù)據(jù)量在1000的時候,由之前的816s,提升到424s,快了一倍,且請求池大小加大的時候,效果更明顯,由于第三方平臺同時建立連接的數(shù)據(jù)限制,我們設(shè)定了40的閾值??梢钥吹?,優(yōu)化的程度很顯著
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Python 使用folium繪制leaflet地圖的實現(xiàn)方法
今天小編就為大家分享一篇Python 使用folium繪制leaflet地圖的實現(xiàn)方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-07-07python2.7實現(xiàn)爬蟲網(wǎng)頁數(shù)據(jù)
這篇文章主要為大家詳細介紹了python2.7實現(xiàn)爬蟲網(wǎng)頁數(shù)據(jù),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-05-05Caffe卷積神經(jīng)網(wǎng)絡(luò)視覺層Vision?Layers及參數(shù)詳解
這篇文章主要為大家介紹了Caffe卷積神經(jīng)網(wǎng)絡(luò)視覺層Vision?Layers及參數(shù)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06python統(tǒng)計多維數(shù)組的行數(shù)和列數(shù)實例
今天小編就為大家分享一篇python統(tǒng)計多維數(shù)組的行數(shù)和列數(shù)實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-06-06