重慶分公司,新征程啟航
為企業提供網站建設、域名注冊、服務器等服務
為企業提供網站建設、域名注冊、服務器等服務
如果你厭倦了多線程,不妨試試python的異步編程,再引入async, await關鍵字之后語法變得更加簡潔和直觀,又經過幾年的生態發展,現在是一個很不錯的并發模型。
10年積累的成都網站制作、做網站經驗,可以快速應對客戶對網站的新想法和需求。提供各種問題對應的解決方案。讓選擇我們的客戶得到更好、更有力的網絡服務。我雖然不認識你,你也不認識我。但先網站設計后付款的網站建設流程,更有儋州免費網站建設讓你可以放心的選擇與我們合作。
下面介紹一下python異步編程的方方面面。
因為GIL的存在,所以Python的多線程在CPU密集的任務下顯得無力,但是對于IO密集的任務,多線程還是足以發揮多線程的優勢的,而異步也是為了應對IO密集的任務,所以兩者是一個可以相互替代的方案,因為設計的不同,理論上異步要比多線程快,因為異步的花銷更少, 因為不需要額外系統申請額外的內存,而線程的創建跟系統有關,需要分配一定量的內存,一般是幾兆,比如linux默認是8MB。
雖然異步很好,比如可以使用更少的內存,比如更好地控制并發(也許你并不這么認為:))。但是由于async/await 語法的存在導致與之前的語法有些割裂,所以需要適配,需要付出額外的努力,再者就是生態遠遠沒有同步編程強大,比如很多庫還不支持異步,所以你需要一些額外的適配。
為了不給其他網站帶來困擾,這里首先在自己電腦啟動web服務用于測試,代碼很簡單。
本文所有依賴如下:
所有依賴可通過代碼倉庫的requirements.txt一次性安裝。
首先看一個錯誤的例子
輸出如下:
發現花費了3秒,不符合預期呀。。。。這是因為雖然用了協程,但是每個協程是串行的運行,也就是說后一個等前一個完成之后才開始,那么這樣的異步代碼并沒有并發,所以我們需要讓這些協程并行起來
為了讓代碼變動的不是太多,所以這里用了一個笨辦法來等待所有任務完成, 之所以在main函數中等待是為了不讓ClientSession關閉, 如果你移除了main函數中的等待代碼會發現報告異常 RuntimeError: Session is closed ,而代碼里的解決方案非常的不優雅,需要手動的等待,為了解決這個問題,我們再次改進代碼。
這里解決的方式是通過 asyncio.wait 方法等待一個協程列表,默認是等待所有協程結束后返回,會返回一個完成(done)列表,以及一個待辦(pending)列表。
如果我們不想要協程對象而是結果,那么我們可以使用 asyncio.gather
結果輸出如下:
通過 asyncio.ensure_future 我們就能創建一個協程,跟調用一個函數差別不大,為了等待所有任務完成之后退出,我們需要使用 asyncio.wait 等方法來等待,如果只想要協程輸出的結果,我們可以使用 asyncio.gather 來獲取結果。
雖然前面能夠隨心所欲的創建協程,但是就像多線程一樣,我們也需要處理協程之間的同步問題,為了保持語法及使用情況的一致,多線程中用到的同步功能,asyncio中基本也能找到, 并且用法基本一致,不一致的地方主要是需要用異步的關鍵字,比如 async with/ await 等
通過鎖讓并發慢下來,讓協程一個一個的運行。
輸出如下:
通過觀察很容易發現,并發的速度因為鎖而慢下來了,因為每次只有一個協程能獲得鎖,所以并發變成了串行。
通過事件來通知特定的協程開始工作,假設有一個任務是根據http響應結果選擇是否激活。
輸出如下:
可以看到事件(Event)等待者都是在得到響應內容之后輸出,并且事件(Event)可以是多個協程同時等待。
上面的事件雖然很棒,能夠在不同的協程之間同步狀態,并且也能夠一次性同步所有的等待協程,但是還不夠精細化,比如想通知指定數量的等待協程,這個時候Event就無能為力了,所以同步原語中出現了Condition。
輸出如下:
可以看到,前面兩個等待的協程是在同一時刻完成,而不是全部等待完成。
通過創建協程的數量來控制并發并不是非常優雅的方式,所以可以通過信號量的方式來控制并發。
輸出如下:
可以發現,雖然同時創建了三個協程,但是同一時刻只有兩個協程工作,而另外一個協程需要等待一個協程讓出信號量才能運行。
無論是協程還是線程,任務之間的狀態同步還是很重要的,所以有了應對各種同步機制的同步原語,因為要保證一個資源同一個時刻只能一個任務訪問,所以引入了鎖,又因為需要一個任務等待另一個任務,或者多個任務等待某個任務,因此引入了事件(Event),但是為了更精細的控制通知的程度,所以又引入了條件(Condition), 通過條件可以控制一次通知多少的任務。
有時候的并發需求是通過一個變量控制并發任務的并發數而不是通過創建協程的數量來控制并發,所以引入了信號量(Semaphore),這樣就可以在創建的協程數遠遠大于并發數的情況下讓協程在指定的并發量情況下并發。
不得不承認異步編程相比起同步編程的生態要小的很多,所以不可能完全異步編程,因此需要一種方式兼容。
多線程是為了兼容同步得代碼。
多進程是為了利用CPU多核的能力。
輸出如下:
可以看到總耗時1秒,說明所有的線程跟進程是同時運行的。
下面是本人使用過的一些異步庫,僅供參考
web框架
http客戶端
數據庫
ORM
雖然異步庫發展得還算不錯,但是中肯的說并沒有覆蓋方方面面。
雖然我鼓勵大家嘗試異步編程,但是本文的最后卻是讓大家謹慎的選擇開發環境,如果你覺得本文的并發,同步,兼容多線程,多進程不值得一提,那么我十分推薦你嘗試以異步編程的方式開始一個新的項目,如果你對其中一些還有疑問或者你確定了要使用的依賴庫并且大多數是沒有異步庫替代的,那么我還是建議你直接按照自己擅長的同步編程開始。
異步編程雖然很不錯,不過,也許你并不需要。
yield相當于return,他將相應的值返回給調用next()或者send()的調用者,從而交出了CPU使用權,而當調用者再次調用next()或者send()的時候,又會返回到yield中斷的地方,如果send有參數,還會將參數返回給yield賦值的變量,如果沒有就和next()一樣賦值為None。但是這里會遇到一個問題,就是嵌套使用generator時外層的generator需要寫大量代碼,看如下示例:?
注意以下代碼均在Python3.6上運行調試
#!/usr/bin/env python# encoding:utf-8def inner_generator():
i = 0
while True:
i = yield i ? ? ? ?if i 10: ? ? ? ? ? ?raise StopIterationdef outer_generator():
print("do something before yield")
from_inner = 0
from_outer = 1
g = inner_generator()
g.send(None) ? ?while 1: ? ? ? ?try:
from_inner = g.send(from_outer)
from_outer = yield from_inner ? ? ? ?except StopIteration: ? ? ? ? ? ?breakdef main():
g = outer_generator()
g.send(None)
i = 0
while 1: ? ? ? ?try:
i = g.send(i + 1)
print(i) ? ? ? ?except StopIteration: ? ? ? ? ? ?breakif __name__ == '__main__':
main()1234567891011121314151617181920212223242526272829303132333435363738394041
為了簡化,在Python3.3中引入了yield from
yield from
使用yield from有兩個好處,
1、可以將main中send的參數一直返回給最里層的generator,?
2、同時我們也不需要再使用while循環和send (), next()來進行迭代。
我們可以將上邊的代碼修改如下:
def inner_generator():
i = 0
while True:
i = yield i ? ? ? ?if i 10: ? ? ? ? ? ?raise StopIterationdef outer_generator():
print("do something before coroutine start") ? ?yield from inner_generator()def main():
g = outer_generator()
g.send(None)
i = 0
while 1: ? ? ? ?try:
i = g.send(i + 1)
print(i) ? ? ? ?except StopIteration: ? ? ? ? ? ?breakif __name__ == '__main__':
main()1234567891011121314151617181920212223242526
執行結果如下:
do something before coroutine start123456789101234567891011
這里inner_generator()中執行的代碼片段我們實際就可以認為是協程,所以總的來說邏輯圖如下:?
接下來我們就看下究竟協程是啥樣子
協程coroutine
協程的概念應該是從進程和線程演變而來的,他們都是獨立的執行一段代碼,但是不同是線程比進程要輕量級,協程比線程還要輕量級。多線程在同一個進程中執行,而協程通常也是在一個線程當中執行。它們的關系圖如下:
我們都知道Python由于GIL(Global Interpreter Lock)原因,其線程效率并不高,并且在*nix系統中,創建線程的開銷并不比進程小,因此在并發操作時,多線程的效率還是受到了很大制約的。所以后來人們發現通過yield來中斷代碼片段的執行,同時交出了cpu的使用權,于是協程的概念產生了。在Python3.4正式引入了協程的概念,代碼示例如下:
import asyncio# Borrowed from countdown(number, n):
while n 0:
print('T-minus', n, '({})'.format(number)) ? ? ? ?yield from asyncio.sleep(1)
n -= 1loop = asyncio.get_event_loop()
tasks = [
asyncio.ensure_future(countdown("A", 2)),
asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()12345678910111213141516
示例顯示了在Python3.4引入兩個重要概念協程和事件循環,?
通過修飾符@asyncio.coroutine定義了一個協程,而通過event loop來執行tasks中所有的協程任務。之后在Python3.5引入了新的async await語法,從而有了原生協程的概念。
async await
在Python3.5中,引入了ayncawait 語法結構,通過”aync def”可以定義一個協程代碼片段,作用類似于Python3.4中的@asyncio.coroutine修飾符,而await則相當于”yield from”。
先來看一段代碼,這個是我剛開始使用asyncawait語法時,寫的一段小程序。
#!/usr/bin/env python# encoding:utf-8import asyncioimport requestsimport time
async def wait_download(url):
response = await requets.get(url)
print("get {} response complete.".format(url))
async def main():
start = time.time()
await asyncio.wait([
wait_download(""),
wait_download(""),
wait_download("")])
end = time.time()
print("Complete in {} seconds".format(end - start))
loop = asyncio.get_event_loop()
loop.run_until_complete(main())12345678910111213141516171819202122232425
這里會收到這樣的報錯:
Task exception was never retrieved
future: Task finished coro=wait_download() done, defined at asynctest.py:9 exception=TypeError("object Response can't be used in 'await' expression",)
Traceback (most recent call last):
File "asynctest.py", line 10, in wait_download
data = await requests.get(url)
TypeError: object Response can't be used in 'await' expression123456
這是由于requests.get()函數返回的Response對象不能用于await表達式,可是如果不能用于await,還怎么樣來實現異步呢??
原來Python的await表達式是類似于”yield from”的東西,但是await會去做參數檢查,它要求await表達式中的對象必須是awaitable的,那啥是awaitable呢? awaitable對象必須滿足如下條件中其中之一:
1、A native coroutine object returned from a native coroutine function .
原生協程對象
2、A generator-based coroutine object returned from a function decorated with types.coroutine() .
types.coroutine()修飾的基于生成器的協程對象,注意不是Python3.4中asyncio.coroutine
3、An object with an await method returning an iterator.
實現了await method,并在其中返回了iterator的對象
根據這些條件定義,我們可以修改代碼如下:
#!/usr/bin/env python# encoding:utf-8import asyncioimport requestsimport time
async def download(url): # 通過async def定義的函數是原生的協程對象
response = requests.get(url)
print(response.text)
async def wait_download(url):
await download(url) # 這里download(url)就是一個原生的協程對象
print("get {} data complete.".format(url))
async def main():
start = time.time()
await asyncio.wait([
wait_download(""),
wait_download(""),
wait_download("")])
end = time.time()
print("Complete in {} seconds".format(end - start))
loop = asyncio.get_event_loop()
loop.run_until_complete(main())123456789101112131415161718192021222324252627282930
好了現在一個真正的實現了異步編程的小程序終于誕生了。?
而目前更牛逼的異步是使用uvloop或者pyuv,這兩個最新的Python庫都是libuv實現的,可以提供更加高效的event loop。
uvloop和pyuv
pyuv實現了Python2.x和3.x,但是該項目在github上已經許久沒有更新了,不知道是否還有人在維護。?
uvloop只實現了3.x, 但是該項目在github上始終活躍。
它們的使用也非常簡單,以uvloop為例,只需要添加以下代碼就可以了
import asyncioimport uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())123
1,我們公司一開始帶飯的人不是很多,公司只有一個茶水間,于是行政的小姐姐很體貼的買了一個微波爐放到茶水間,然后大家中午可以在茶水間排隊去熱飯。很開心。
2,后來公司發展越來越好,不斷有新鮮的血液注入到公司,充滿了活力,帶飯的人越來越多,行政小姐姐發現一個微波爐根本不夠用,于是又賣了兩個,大家中午繼續去茶水間排隊去熱飯,速度比以前快了許多,大大緩解了排隊過長的壓力。
3,某一天公司上市了,發展一片大好,人員也越來越多,于是公司換了新的辦公地點,將原來的一個茶水間擴充到了3個,微波爐每個茶水間放5個,同時雇了一個阿姨,專門輔助員工的一些生活方面的事,讓員工可以安安心心工作,于是變成了這樣,到中午的時候,員工選擇3個茶水間一個,將飯盒放在茶水間里,阿姨負責將飯熱好放到指定位置,一個阿姨就可以同時操作多個微波爐,一個熱好后取出放入下一個,大大提高了熱飯效率,熱好的飯可以放到指定位置一會自己來取即可,也可以送到員工的位置,大家中午再也不用排隊熱飯了,
多個茶水間相當于多進程(放在python也可以理解為多核),大大提高了效率,但同時開銷也很多,增加一個茶水間的代價遠大于增加一個微波爐。
進程,直觀點說,保存在硬盤上的程序運行以后,會在內存空間里形成一個獨立的內存體,這個內存體 有自己獨立的地址空間,有自己的堆 ,上級掛靠單位是操作系統。 操作系統會以進程為單位,分配系統資源(CPU時間片、內存等資源),進程是資源分配的最小單位 。
大家排隊去茶水間熱飯,先到的先熱,同一茶水間同時只有一個人在熱飯。即使有多個微波爐也是順序的開始工作,三個微波爐相當于三個線程,同時可以熱三份飯,但熱飯的人是順序進入茶水間的。
線程,有時被稱為輕量級進程(Lightweight Process,LWP),是操作系統調度(CPU調度)執行的最小單位 。
阿姨可以同時操作多個微波爐,一個熱好后取出放入下一個,大大提高了熱飯效率,哪個微波爐熱好就先用哪個,所以協程是無序的,大大提高了工作效率。
一個阿姨在多個茶水間和微波爐工作 充分利用多核
協程,是一種比線程更加輕量級的存在,協程不是被操作系統內核所管理,而完全是由程序所控制(也就是在用戶態執行)。這樣帶來的好處就是性能得到了很大的提升,不會像線程切換那樣消耗資源。
協程在子程序內部是可中斷的,然后轉而執行別的子程序,在適當的時候再返回來接著執行 。
阿姨熱好飯之后放到指定位置,每個人可以根據自己的時間過來取,如果阿姨空閑了也可以送到員工工位。
公司初期人員較少,所有員工在唯一的一個茶水間排隊使用同一個微波爐,順序使用微波爐。
公司中期增加了微波爐的數量,所有員工在茶水間排隊使用多個微波爐,多個微波爐同時工作。
公司發展后期增加了茶水間和微波爐的數量,只有一個阿姨使用多個茶水間的微波爐,只有在微波爐可以使用的條件下才去使用,其他時間可以干其他事情。
【區別】:
調度 : 線程作為調度和分配的基本單位,進程作為擁有資源的基本單位 ;
并發性 : 不僅進程之間可以并發執行,同一個進程的多個線程之間也可并發執行 ;
擁有資源 : 進程是擁有資源的一個獨立單位,線程不擁有系統資源 ,但可以訪問隸屬于進程的資源。進程所維護的是程序所包含的資源(靜態資源), 如: 地址空間,打開的文件句柄集,文件系統狀態,信號處理handler等 ;線程所維護的運行相關的資源(動態資源),如: 運行棧,調度相關的控制信息,待處理的信號集等 ;
系統開銷 :在創建或撤消進程時,由于系統都要為之分配和回收資源,導致系統的開銷明顯大于創建或撤消線程時的開銷。但是進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個進程死掉就等于所有的線程死掉,所以 多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些 。
【聯系】:
一個線程只能屬于一個進程,而一個進程可以有多個線程,但至少有一個線程 ;
資源分配給進程,同一進程的所有線程共享該進程的所有資源;
處理機分給線程,即 真正在處理機上運行的是線程 ;
線程在執行過程中,需要協作同步。不同進程的線程間要利用消息通信的辦法實現同步。
協程的特點在于是一個線程執行,那和多線程比,協程有何 優勢 ?
極高的執行效率 :因為 子程序切換不是線程切換,而是由程序自身控制 ,因此, 沒有線程切換的開銷 ,和多線程比,線程數量越多,協程的性能優勢就越明顯;
不需要多線程的鎖機制 :因為只有一個線程,也不存在同時寫變量沖突, 在協程中控制共享資源不加鎖 ,只需要判斷狀態就好了,所以執行效率比多線程高很多。
(1)進程可使用multiprocessing包實現
與threading.Thread類似,它可以利用multiprocessing.Process對象來創建一個進程。
該進程可以運行在Python程序內部編寫的函數。
該Process對象與Thread對象的用法相同,也有start(), run(), join()的方法。
(2)線程可使用threading包或thread包
(3)協程通過async await 實現,async聲明一個函數為異步函數,await可將程序掛起,去執行其他的異步程序。協程 有兩種,一種 無棧協程,python 中 以 asyncio 為代表, 一種有棧協程,python 中 以 gevent 為代表。
(1)以多進程形式,允許多個任務同時運行;
(2)以多線程形式,允許單個任務分成不同的部分運行;
(3)提供協調機制,一方面防止進程之間和線程之間產生沖突,另一方面允許進程之間和線程之間共享資源。
可以使用切片獲取部分數據;
元組的值一旦設置:
{}表示字典,[]是數組,()是元組;
數組的值可以改變區別如下,不可更改,不可使用切片
異步是計算機多線程的異步處理。與同步處理相對,異步處理不用阻塞當前線程來等待處理完成,而是允許后續操作,直至其它線程將處理完成,并回調通知此線程。
await的解釋:
await用來聲明程序掛起。
比如異步程序執行到某一步時需要等待的時間很長,就將此掛起,去執行其他的異步程序。
await 后面只能跟異步程序或有__await__屬性的對象,因為異步程序與一般程序不同。
程序解釋:
假設有兩個異步函數async a,async b,a中的某一步有await,
當程序碰到關鍵字await b()后,異步程序掛起后去執行另一個異步b程序,就是從函數內部跳出去執行其他函數,
當掛起條件消失后,不管b是否執行完,要馬上從b程序中跳出來,回到原程序執行原來的操作。
如果await后面跟的b函數不是異步函數,那么操作就只能等b執行完再返回,無法在b執行的過程中返回。
如果要在b執行完才返回,也就不需要用await關鍵字了,直接調用b函數就行。
所以這就需要await后面跟的是異步函數了。
在一個異步函數中,可以不止一次掛起,也就是可以用多個await。
更多Python知識,請關注:Python自學網!!