Python異步與 JavaScript 原生異步有什么區(qū)別?
眾所周知,JavaScript 是單線程的,所以瀏覽器通過(guò) JavaScript 發(fā)起的請(qǐng)求是異步請(qǐng)求。Python 自帶的 asyncio 模塊為 Python 帶來(lái)了原生的異步能力。
在學(xué)習(xí) asyncio 時(shí),我們應(yīng)當(dāng)正確認(rèn)識(shí)到異步代碼在 Python 中與 JavaScript 原生代碼中有什么區(qū)別,這樣才能更好地理解Python中用同步代碼寫(xiě)異步程序這個(gè)邏輯。
對(duì)于異步操作,我們?nèi)绻褂萌粘I钪械睦樱赡軙?huì)幫助我們理解 JavaScript 原生的異步操作,但是卻有可能阻礙我們理解 Python 的異步操作。
例如:我把洗衣機(jī)打開(kāi),等待洗衣機(jī)自動(dòng)運(yùn)行的這段時(shí)間,我可以去煮飯,等待飯煮好的這個(gè)過(guò)程,我可以去看書(shū)。
現(xiàn)在假設(shè)我們要請(qǐng)求一個(gè)網(wǎng)址:http://httpbin.org/delay/5,這個(gè)網(wǎng)址請(qǐng)求以后,需要等待5秒鐘才會(huì)返回結(jié)果。我們使用 jQuery來(lái)寫(xiě)一段 JavaScript 代碼:
- function test_async(){
- $.ajax({type: 'GET',
- contentType: 'application/json; charset=utf-8',
- url: 'http://httpbin.org/delay/5',
- success: function (response) {
- console.log('5秒請(qǐng)求返回:', response)
- }
- })
- var a = 1 + 1
- a = a * 2
- console.log(a)
- $.ajax({type: 'GET',
- contentType: 'application/json; charset=utf-8',
- url: 'http://httpbin.org/ip',
- success: function (response) {
- console.log('查詢 IP 返回:', response)
- }
- })
- console.log('這里是代碼的末尾')
- }
運(yùn)行效果如下圖所示:
可以看出來(lái),整個(gè)代碼的執(zhí)行邏輯與我們生活中的異步是一致的,首先發(fā)起了一個(gè)5秒的請(qǐng)求,但是程序不會(huì)卡住等待,而是繼續(xù)運(yùn)行后面的代碼,然后發(fā)起新的請(qǐng)求。由于新的請(qǐng)求返回時(shí)間短,所以新的請(qǐng)求很快返回并打印,最后才是打印的5秒請(qǐng)求的返回結(jié)果。
這就像是我們打開(kāi)了洗衣機(jī)的電源,然后去淘米煮飯,米放進(jìn)了電飯鍋,打開(kāi)電飯鍋電源,然后去看書(shū),最后飯先煮好,然后衣服再洗完。
JavaScript 原生的異步請(qǐng)求的過(guò)程,與日常生活中的邏輯很像。所以很容易就能理解 JavaScript 的異步流程。
但是 Python 里面,異步又是另外一種情況了。
我們來(lái)寫(xiě)一段代碼:
- import asyncio
- import aiohttp
- async def main():
- async with aiohttp.ClientSession() as client:
- response = await client.get('http://httpbin.org/delay/5')
- result = await response.json()
- print('5秒請(qǐng)求返回:', result)
- a = 1 + 1
- a = a * 2
- print(a)
- new_response = await client.get('http://httpbin.org/ip')
- new_result = await new_response.json()
- print('查詢 IP 返回:', new_result)
- print('這里是代碼的末尾')
- asyncio.run(main())
運(yùn)行效果如下圖所示:
可以看出,程序依然是串行運(yùn)行的,根本就沒(méi)有異步痕跡。
要讓程序異步運(yùn)行,我們需要湊夠一批任務(wù)提交給 asyncio,讓它自己通過(guò)事件循環(huán)來(lái)調(diào)度這些任務(wù):
- import asyncio
- import aiohttp
- async def do_plus():
- a = 1 + 1
- a = a * 2
- print(a)
- async def test_delay(client):
- response = await client.get('http://httpbin.org/delay/5')
- result = await response.json()
- print('5秒請(qǐng)求返回:', result)
- async def test_ip(client):
- response = await client.get('http://httpbin.org/ip')
- result = await response.json()
- print('查詢 IP 返回:', result)
- async def test_print():
- print('這里是代碼的末尾')
- async def main():
- async with aiohttp.ClientSession() as client:
- tasks = [
- asyncio.create_task(test_delay(client)),
- asyncio.create_task(do_plus()),
- asyncio.create_task(test_ip(client)),
- asyncio.create_task(test_print())
- ]
- await asyncio.gather(*tasks)
- asyncio.run(main())
運(yùn)行效果如下圖所示:
這是由于,在asyncio 里面,task是可以并行的最小單位,并且,task 要湊夠一批一起通過(guò)asyncio.gather或者asyncio.wait提交給事件循環(huán)以后,才能并行起來(lái)。
當(dāng)使用代碼asyncio.create_task(異步函數(shù)())的時(shí)候,這個(gè)異步函數(shù)實(shí)際上并沒(méi)有真正運(yùn)行,所以,在上面的代碼中:
- tasks = [
- asyncio.create_task(test_delay(client)),
- asyncio.create_task(do_plus()),
- asyncio.create_task(test_ip(client)),
- asyncio.create_task(test_print())
- ]
創(chuàng)建了一個(gè)包含4個(gè)task 的列表,此時(shí)這4個(gè)異步函數(shù)中的代碼都還沒(méi)有執(zhí)行。
當(dāng)再調(diào)用await asyncio.gather(*tasks)時(shí),這4個(gè)任務(wù)被作為4個(gè)參數(shù)傳入到了 asyncio.gather函數(shù)中,于是 Python 的事件循環(huán)開(kāi)始調(diào)度他們。在這些異步函數(shù)中,包含await的地方,就是在告訴 Python,await后面的這個(gè)函數(shù)可能會(huì)有 IO 等待,可以掛起等一會(huì)再來(lái)看,現(xiàn)在可以去檢查事件循環(huán)里面其他異步任務(wù)是否已經(jīng)結(jié)束等待可以運(yùn)行。而沒(méi)有 await的地方依然是串行的,例如do_plus里面的三行代碼就是按順序一次性運(yùn)行完成的。
所以,當(dāng)我們使用 Python 的 asyncio 寫(xiě)異步代碼時(shí),我們需要提前安排好異步的切換位置并包裝為異步任務(wù),然后把一批任務(wù)一次性提交給 asyncio,讓 Python 自己根據(jù)我們安排好的切換邏輯來(lái)調(diào)度這些任務(wù)。
這就像是,當(dāng)我寫(xiě) JavaScript 的時(shí)候,我親自上陣先把洗衣機(jī)電源打開(kāi),然后我再來(lái)考慮接下來(lái)要利用等待時(shí)間做什么事情。
當(dāng)我寫(xiě) Python 的時(shí)候,我需要提前把整個(gè)計(jì)劃都安排好:先打開(kāi)洗衣機(jī),在等待的時(shí)間淘米煮飯,然后再看書(shū)。并把這個(gè)計(jì)劃表提交給一個(gè)專(zhuān)門(mén)做事情的人來(lái)執(zhí)行。
理解了這個(gè)差別,才能更好地在 Python 中使用 asyncio。
注意,本文說(shuō)到的 JavaScript異步,是 JavaScript 最原始的異步邏輯?,F(xiàn)在 JavaScript 有 Promise 等等高級(jí)功能,實(shí)現(xiàn)類(lèi)似于 Python 的這種異步邏輯。