Python - 從使用執行緒到使用 async/await
async/await
是一種非同步變成方法,還有兩種你可能聽過,
1. 回撥
(寫過 JavaScript 的肯定很熟悉了)
非同步意味著任務不會阻塞,比如,如果我要下載一個比較忙的網路資源,我的程式不需要一直等待下載完成,它可以在等待下載時繼續做其他事情。這與並行執行多個操作不同。以下虛擬碼比較容易理解:
# 慢方法 page = get_page_sync('some_page') # 會阻塞整個程式的執行 print(page)
有兩種方法可以改善上述的情況
(一)首先,讓我們試試使用執行緒。通過使用執行緒,我們可以將 get_page_sync
呼叫放到單獨的執行緒去執行,這樣主執行緒 就可以繼續執行其他操作。
# 將慢方法放到單獨的執行緒執行 t = threading.thread( target = get_page_sync('some_page',args=('some_page',)) ) t.run() # 線上程執行時執行其他操作 do_something_else() # 等待執行緒完執行成 t.join()
執行緒有幾個優缺點,主要的缺點是:
1. 必須在改變共享資料前鎖定共享資料
(二)現在我們試試第二種中的 async/await,Python3.5 開始支援的 async/await 方式,與第一種(執行緒)之間的主要區別在於,後者是作業系統核心執行上下文切換,而前者中我們自己控制。(上下文切換即,當多個執行緒正在執行時,核心可能停止當前程序,使其進入休眠狀態,並選擇不同的執行緒繼續執行。這被稱作搶佔式多工處理【Preemption】)
當我們自己控制時,它被稱作非搶佔式或合作型多工式,因為是我們自己處理上下文切換,所以我們需要一個排程程式,也叫做『事件迴圈』。此事件迴圈只迴圈遍歷等待中的排程,並執行它的所有事件。每當我們產生操作時,當前任務會被新增到佇列中,且第一個任務(優先順序而非順序)從佇列中彈出並開始執行。例如,可以通過以下方式更改上述虛擬碼:
async def print_page(): page = await get_page_sync('some_page') print(page)
當我們觸發上面的語句時, get_page_async
方法將非阻塞的獲取 some_page
還有 yield 控制代碼,這意味著我們的 print_page
函式將控制時間迴圈 ,並且時間迴圈可以繼續執行其他曹組,知道我們得到返回的響應。
我們先將我們的執行緒程式碼改造成這種語法。我們將使用 asyncio(Python 自帶的時間迴圈庫),並使用 aiohttp
包來執行非同步 http 請求。
我們將會建立一個名為 main
函式,它將成為我們非同步程式碼的入口。然後我們建立一個時間迴圈和一個「未來物件」。這個未來物件是對非同步函式的抽象,它儲存了一些基本的屬性,比如它當前的狀態(就像 Promise 一樣) 。然後我們將告訴我們的時間迴圈繼續執行,知道這個「未來」完成。
loop = asyncio.get_event_loop() future = asyncio.ensure_future(main()) loop.run_until_complete(future)
在我們的 main
方法中,我們將建立另一個未來任務列表,每個任務負責從某網站下載不同的桐鄉。我們這樣做是因為每次下載都會發起網路請求,在網路請求時,我們可以執行另一端程式碼。建立任務列表後,我們可以通過呼叫等待整個列表執行完成 asyncio.gather
,這就是它的實現:
async def main(): tasks = [] async with aiohttp.ClientSession() as session: for img in img_list: task = asyncio.ensure_future(download_img(img, session)) task.append(task) await asyncio.gather(*tasks)
(這段程式碼來的有點猛了)
最後一個我們要改的方法就是 download_img
了,我們僅僅需要替換 requests.get
呼叫為非同步:
i = 1 async def download_img(img, session): global i, bar # 獲取檔案字尾 file_ext = get_extention(img.link) # 拼接檔名 file_name = img.id + file_ext resp = await session.get(img.link) with open(file_name, 'wb') as f: async for chunk in resp.content.iter_chunked(1024): f.write(chunk) bar.update(i) i += 1
要注意的一點是在更新 i 的時候不需要先鎖住它,這是因為我們前面說過,沒有程式碼是同時執行的,所以永遠不可能出現競態條件。
因為沒有鎖或者執行緒的開銷,非同步版本可能還會比多執行緒版本快一些。
這是完整程式碼:
#! /usr/bin/env python import os import re import sys import aiohttp import asyncio import async_timeout import progressbar from imgurpython import ImgurClient regex = re.compile(r'\.(\w+)$') def get_extension(link): ext = regex.search(link).group() return ext i = 1 async def download_img(img, session): global i, bar # get the file extension file_ext = get_extension(img.link) # create unique name by combining file id with its extension file_name = img.id + file_ext resp = await session.get(img.link) with open(file_name, 'wb') as f: async for chunk in resp.content.iter_chunked(1024): f.write(chunk) bar.update(i) i += 1 try: album_id = sys.argv[1] except IndexError: raise Exception('Please specify an album id') client_id = os.getenv('IMGUR_CLIENT_ID') client_secret = os.getenv('IMGUR_CLIENT_SECRET') client = ImgurClient(client_id, client_secret) img_lst = client.get_album_images(album_id) bar = progressbar.ProgressBar(max_value=len(img_lst)) async def main(): tasks = [] async with aiohttp.ClientSession() as session: for img in img_lst: task = asyncio.ensure_future(download_img(img, session)) tasks.append(task) await asyncio.gather(*tasks) loop = asyncio.get_event_loop() future = asyncio.ensure_future(main()) loop.run_until_complete(future)
原文: ofollow,noindex"> https:// medium.com/@exqu17/pyth on-bits-moving-from-threads-to-async-await-741ec5124cdc
作者: https:// medium.com/@exqu17? source=post_header_lockup
我一般想了解什麼的時候,就會找一篇近期的國外文章翻譯一下,為什麼呢,很簡單,因為老外寫的文件一般都淺顯易懂,英文措辭雖囉嗦但歧義很少,最重要的是,文章也能幫助其他小夥伴,關注專欄,一起成長