Python異步編程入門協(xié)程到底是什么與線程、進(jìn)程的區(qū)別
Python異步編程入門:協(xié)程到底是什么?
你可能已經(jīng)遇到過這樣的場景:寫了一個爬蟲,但請求網(wǎng)頁時總是卡??;或者搭了個Web服務(wù),同時處理幾個請求就變得慢吞吞。這時候別人可能會告訴你:“試試異步編程吧,用協(xié)程。”
但協(xié)程到底是什么?它跟線程、進(jìn)程有什么區(qū)別?為什么大家都在說asyncio?今天咱們就來好好聊聊這個話題。
從瓶頸說起:程序為什么卡?
想象一下你去快餐店點餐。如果只有一個收銀員,前面的人點單特別慢,你就得一直等著——這就是典型的同步阻塞。你的代碼可能正在等網(wǎng)絡(luò)響應(yīng)、等文件讀寫、等數(shù)據(jù)庫查詢,而CPU就在那里空轉(zhuǎn)。
換成多線程呢?好比開了幾個收銀臺,同時服務(wù)。但每個收銀臺還是要等顧客慢慢掏錢、找零,而且收銀臺之間還要協(xié)調(diào)(比如共用一臺打印機),協(xié)調(diào)不好就會出問題。在程序里,這就是線程安全問題和上下文切換開銷。
協(xié)程的做法不太一樣:一個收銀員同時接待多個顧客。A顧客在掏錢包時,收銀員轉(zhuǎn)頭問B顧客要什么;B顧客猶豫時,又去給C顧客結(jié)賬。看起來收銀員一直在忙,但實際沒有真的“同時”做多件事,只是切換得很快。
協(xié)程的本質(zhì):可暫停的函數(shù)
協(xié)程本質(zhì)上是一種特殊的函數(shù),它能在執(zhí)行到一半時暫停,把控制權(quán)交出去,過一會兒又能從暫停的地方繼續(xù)執(zhí)行。
async def fetch_data():
print("開始請求數(shù)據(jù)")
await asyncio.sleep(2) # 在這里暫停,讓其他協(xié)程運行
print("數(shù)據(jù)返回了")
這個await asyncio.sleep(2)就像告訴程序:“我先歇會兒,你去干點別的,2秒后再叫我。”這種機制讓單個線程可以處理多個任務(wù),而不是傻等。
你可能會問,這和生成器(generator)的yield有點像吧?確實,Python的協(xié)程就是從生成器進(jìn)化來的。早期的協(xié)程就是用yield實現(xiàn)的,但async/await語法更清晰,專門為異步編程設(shè)計。
協(xié)程 vs 線程 vs 進(jìn)程:什么時候選哪個?
先看個簡單的對比:
- 多進(jìn)程:開多個廚房,各自獨立,但溝通成本高(進(jìn)程間通信)。適合CPU密集型計算,能利用多核優(yōu)勢。
- 多線程:一個廚房里多個廚師,共用設(shè)備,但要小心互相干擾(線程安全)。適合I/O操作,但線程數(shù)量多了開銷大。
- 協(xié)程:一個廚師同時照看幾口鍋,哪口鍋需要攪拌就攪一下,不需要時就處理別的。適合高并發(fā)I/O,但單個協(xié)程不能阻塞。
協(xié)程最大的優(yōu)勢就是輕量。創(chuàng)建一個線程需要幾MB內(nèi)存,而一個協(xié)程可能只要幾KB。一個線程能跑成千上萬個協(xié)程,切換開銷極小,特別適合I/O密集的場景——比如網(wǎng)絡(luò)請求、文件讀寫,這些操作大部分時間都在等,而不是真的在計算。
但協(xié)程不是銀彈。如果你的任務(wù)是純計算型的(比如圖像處理、復(fù)雜算法),協(xié)程幫不上什么忙,因為計算本身不會主動讓出CPU。這時候多進(jìn)程或者直接優(yōu)化算法可能更有效。
Python協(xié)程的演進(jìn):從yield到async/await
Python實現(xiàn)協(xié)程的方式有過幾次大的變化,了解這段歷史有助于理解為什么現(xiàn)在是這樣的設(shè)計:
# 1. 生成器時代(Python 2.5+) - 用yield模擬
def old_style_coroutine():
print("開始")
data = yield "請給我數(shù)據(jù)"
print(f"收到數(shù)據(jù):{data}")
# 使用方式:
coro = old_style_coroutine()
next(coro) # 啟動,輸出"開始",返回"請給我數(shù)據(jù)"
coro.send("hello") # 發(fā)送數(shù)據(jù),輸出"收到數(shù)據(jù):hello"
# 2. 裝飾器時代(Python 3.4)- 引入asyncio
@asyncio.coroutine
def decorator_style():
yield from asyncio.sleep(1)
print("完成")
# 3. 現(xiàn)代寫法(Python 3.5+)- 現(xiàn)在的標(biāo)準(zhǔn)
async def modern_coroutine():
await asyncio.sleep(1)
print("完成")現(xiàn)在基本都用async/await這套語法,清晰直觀。但你可能在舊代碼里看到前兩種寫法,知道它們是一回事就行。有趣的是,async/await在C#、JavaScript等語言中也是類似的語法,學(xué)會一次,多語言受益。
深入理解async和await
第一次見async def可能會有點懵:這函數(shù)怎么調(diào)用后不執(zhí)行?。?/p>
async def hello():
print("Hello, async!")
coro = hello() # 注意:這里不會打印任何東西!
print(type(coro)) # <class 'coroutine'>這里有個重要概念:協(xié)程函數(shù)被調(diào)用時返回的是一個協(xié)程對象,而不是直接執(zhí)行。要讓它跑起來,需要事件循環(huán)的調(diào)度。這就像給了你一張任務(wù)卡,但需要有人(事件循環(huán))來執(zhí)行它。
await則是協(xié)程世界里的“等待”符號。但它不是阻塞等待,而是“我這兒暫時沒事,你先去忙別的”。
async def main():
print("開始煮面")
print("水燒開了,下面條")
await asyncio.sleep(3) # 等面煮熟,但程序可以去干別的
print("面熟了,撈起來")
print("開始炒菜")
await asyncio.sleep(2) # 等菜炒熟
print("菜好了")
print("開飯!")有個常見的誤解:await就是異步。其實await本身不創(chuàng)造異步,它只是告訴程序“這里可以切換”。真正的異步能力來自那些支持異步的操作,比如asyncio.sleep()、aiohttp的網(wǎng)絡(luò)請求等。
另一個常見誤區(qū):以為用了async/await就自動變快。實際上,如果所有操作都是順序的await,那和同步?jīng)]什么區(qū)別。真正的并發(fā)要靠asyncio.gather()、asyncio.wait()這樣的結(jié)構(gòu)。
事件循環(huán):協(xié)程的調(diào)度中心
如果協(xié)程是演員,事件循環(huán)就是導(dǎo)演。導(dǎo)演決定哪個演員什么時候上場、什么時候休息。
在Python 3.7之前,你需要自己管理事件循環(huán):
# Python 3.6及以前
import asyncio
async def task():
print("任務(wù)執(zhí)行中")
await asyncio.sleep(1)
# 手動管理事件循環(huán)
loop = asyncio.get_event_loop()
loop.run_until_complete(task())
loop.close()Python 3.7引入了asyncio.run(),簡化了這一切:
# Python 3.7+
async def main():
await task()
asyncio.run(main()) # 一行搞定asyncio.run()幫我們做了三件事:創(chuàng)建新的事件循環(huán)、運行協(xié)程、關(guān)閉循環(huán)。對于大多數(shù)應(yīng)用,這就夠了。
事件循環(huán)的工作方式有點像餐廳的叫號系統(tǒng):不斷檢查有沒有新的“事件”(比如網(wǎng)絡(luò)數(shù)據(jù)到達(dá)、定時器到期),然后喚醒對應(yīng)的協(xié)程繼續(xù)工作。它維護(hù)著一個待辦事項列表,哪個能處理就處理哪個。
實戰(zhàn)對比:同步 vs 異步下載
理論說了這么多,來個實際例子感受一下區(qū)別。
假設(shè)你要下載10張網(wǎng)絡(luò)圖片,用傳統(tǒng)同步方式大概是這樣:
import requests
import time
def download_sync(url, filename):
response = requests.get(url)
with open(filename, 'wb') as f:
f.write(response.content)
urls = [...] # 10個圖片URL
start = time.time()
for i, url in enumerate(urls):
download_sync(url, f"image_{i}.jpg")
print(f"下載完第{i+1}張")
print(f"總耗時: {time.time()-start:.2f}秒")
# 如果每張圖要1秒,這里大概要10秒這是典型的順序執(zhí)行,一張下完再下一張,大部分時間都在等網(wǎng)絡(luò)響應(yīng)。
換成協(xié)程版本:
import aiohttp
import asyncio
import time
async def download_async(session, url, filename):
async with session.get(url) as response:
content = await response.read()
with open(filename, 'wb') as f:
f.write(content)
async def main():
urls = [...] # 同樣的10個URL
async with aiohttp.ClientSession() as session:
tasks = []
for i, url in enumerate(urls):
task = download_async(session, url, f"image_{i}.jpg")
tasks.append(task)
await asyncio.gather(*tasks) # 并發(fā)下載!
start = time.time()
asyncio.run(main())
print(f"總耗時: {time.time()-start:.2f}秒")
# 可能只要1-2秒就全部下載完了區(qū)別很明顯:同步版本是等一張下完再下一張;協(xié)程版本是同時發(fā)起所有請求,哪張先到就先處理哪張。對于I/O密集型任務(wù),速度提升可能非常顯著。
不過要注意,并不是所有場景都能這樣簡單替換。requests庫是同步的,不能直接在協(xié)程里用,需要換用異步版本的aiohttp。這也是很多初學(xué)者容易踩的坑:用了async/await,但調(diào)用的庫不支持異步,結(jié)果還是同步執(zhí)行。
常見的協(xié)程使用場景
什么時候該考慮用協(xié)程呢?這里有幾個典型場景:
- Web服務(wù)器:比如用FastAPI、Sanic或aiohttp框架。每個請求都可能涉及數(shù)據(jù)庫查詢、外部API調(diào)用,用協(xié)程可以同時處理大量連接。
- 網(wǎng)絡(luò)爬蟲:需要爬取大量網(wǎng)頁,每個網(wǎng)頁的下載都是I/O操作,協(xié)程能顯著提高效率。
- 實時通信:聊天應(yīng)用、消息推送,需要維持大量長連接,協(xié)程的內(nèi)存開銷比線程小得多。
- 批量文件處理:比如讀取大量文件、圖片處理(注意:圖片處理本身是CPU密集型,但讀取寫入文件是I/O密集型)。
- 微服務(wù)調(diào)用:一個服務(wù)需要調(diào)用多個其他微服務(wù),然后合并結(jié)果,協(xié)程可以并行發(fā)起所有調(diào)用。
協(xié)程學(xué)習(xí)的難點和坑
剛開始學(xué)協(xié)程時,有幾個常見的困惑點:
第一,忘記加await:
async def get_data():
return "數(shù)據(jù)"
async def main():
result = get_data() # 錯誤!應(yīng)該加await
print(result) # 打印出來是個協(xié)程對象,不是字符串第二,在同步函數(shù)里調(diào)用協(xié)程:
def sync_func():
data = await get_data() # 語法錯誤!await只能在async函數(shù)里用
第三,阻塞事件循環(huán):
async def bad_example():
# 這個函數(shù)會阻塞整個事件循環(huán)!
time.sleep(5) # 應(yīng)該用await asyncio.sleep(5)
# CPU密集型計算也會阻塞
sum(range(10**7)) # 應(yīng)該放到線程池里執(zhí)行第四,以為所有庫都支持異步:實際上很多常用庫(比如requests、pymysql的同步API)都是同步的。需要用異步替代庫(aiohttp、aiomysql)或者把同步調(diào)用放到線程池里。
性能真的提升了嗎?一個簡單測試
我們來做個簡單實驗,看看協(xié)程在I/O密集型任務(wù)上的實際表現(xiàn):
import asyncio
import time
import aiohttp
import requests
# 測試用的URL,請求會延遲1秒返回
TEST_URL = "http://httpbin.org/delay/1"
# 同步版本
def sync_test(n=10):
start = time.time()
for i in range(n):
requests.get(TEST_URL)
return time.time() - start
# 異步版本
async def async_test(n=10):
start = time.time()
async with aiohttp.ClientSession() as session:
tasks = [session.get(TEST_URL) for _ in range(n)]
await asyncio.gather(*tasks)
return time.time() - start
# 運行測試
print("同步版,10個請求:")
print(f"耗時:{sync_test(10):.2f}秒")
print("\n異步版,10個請求:")
print(f"耗時:{asyncio.run(async_test(10)):.2f}秒")在我的測試中,同步版大約需要10秒(順序執(zhí)行,每個1秒),而異步版大約只要1秒多(并發(fā)執(zhí)行)。當(dāng)請求數(shù)量增加到100時,差異會更加明顯。
現(xiàn)在開始用協(xié)程還太早嗎?
如果你的項目主要是CPU密集型計算(比如數(shù)據(jù)分析、圖像處理),協(xié)程帶來的提升可能有限。但如果是Web服務(wù)、爬蟲、聊天機器人這類I/O密集的應(yīng)用,協(xié)程幾乎成了標(biāo)配。
學(xué)習(xí)曲線呢?確實需要一點時間適應(yīng)。從同步思維切換到異步思維,就像從單線程轉(zhuǎn)到多線程一樣,需要重新考慮程序的組織方式。不過一旦掌握,代碼的性能和可讀性都會有很大改善。
Python的異步生態(tài)已經(jīng)相當(dāng)成熟了。Web框架有FastAPI(性能強悍,還自動生成API文檔)、Sanic;數(shù)據(jù)庫有aiomysql、asyncpg;HTTP客戶端有aiohttp;甚至機器學(xué)習(xí)領(lǐng)域也開始出現(xiàn)異步支持。
下一步該學(xué)什么?
今天我們從協(xié)程的基本概念講到了實際應(yīng)用,但這只是異步編程的起點。下次我們會深入更多實用話題:
- 協(xié)程間的通信:多個協(xié)程怎么安全地共享數(shù)據(jù)?
asyncio.Queue怎么用? - 同步原語:協(xié)程版本的鎖(Lock)、信號量(Semaphore)、事件(Event)是什么?
- 錯誤處理:協(xié)程里的異常怎么捕獲和處理?
- 與線程/進(jìn)程結(jié)合:如何在協(xié)程里調(diào)用同步代碼?怎么利用多核CPU?
- 實際項目結(jié)構(gòu):大型異步項目該怎么組織代碼?
這些話題我會在下一篇文章中詳細(xì)講解。如果你已經(jīng)躍躍欲試,建議先從一個小項目開始,比如寫個異步爬蟲,或者用FastAPI搭個簡單的Web服務(wù)。
你平時寫代碼時,遇到過哪些適合用協(xié)程解決的場景?或者對協(xié)程的哪些部分感到困惑?歡迎在評論區(qū)聊聊你的經(jīng)驗或問題,我們一起探討。
到此這篇關(guān)于Python異步編程入門:協(xié)程到底是什么?的文章就介紹到這了,更多相關(guān)Python協(xié)程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python實現(xiàn)查找字符串?dāng)?shù)組最長公共前綴示例
這篇文章主要介紹了Python實現(xiàn)查找字符串?dāng)?shù)組最長公共前綴,涉及Python針對字符串的遍歷、判斷、計算等相關(guān)操作技巧,需要的朋友可以參考下2019-03-03
Python數(shù)據(jù)可視化Pyecharts庫實現(xiàn)桑葚圖效果
這篇文章主要介紹了Python數(shù)據(jù)可視化如何使用Pyecharts庫來實現(xiàn)桑葚圖效果圖,文中給出實現(xiàn)的示例代碼,有需要的朋友可以借鑒參考想,希望能夠有所幫助2021-09-09
使用python+pandas讀寫xlsx格式中的數(shù)據(jù)
這篇文章主要介紹了使用python+pandas讀寫xlsx格式中的數(shù)據(jù),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,感興趣的小伙伴可以參考一下2022-08-08

