­

Python非同步與 JavaScript 原生非同步有什麼區別?

  • 2020 年 3 月 26 日
  • 筆記

與產品經理春遊時撞見的一隻花貓

眾所周知,JavaScript 是單執行緒的,所以瀏覽器通過 JavaScript 發起的請求是非同步請求。Python 自帶的 asyncio 模組為 Python 帶來了原生的非同步能力。

在學習 asyncio 時,我們應當正確認識到非同步程式碼在 Python 中與 JavaScript 原生程式碼中有什麼區別,這樣才能更好地理解Python中用同步程式碼寫非同步程式這個邏輯。

對於非同步操作,我們如果使用日常生活中的例子,可能會幫助我們理解 JavaScript 原生的非同步操作,但是卻有可能阻礙我們理解 Python 的非同步操作。

例如:我把洗衣機打開,等待洗衣機自動運行的這段時間,我可以去煮飯,等待飯煮好的這個過程,我可以去看書。

現在假設我們要請求一個網址:http://httpbin.org/delay/5,這個網址請求以後,需要等待5秒鐘才會返回結果。我們使用 jQuery來寫一段 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秒請求返回:', 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('這裡是程式碼的末尾')  }

運行效果如下圖所示:

可以看出來,整個程式碼的執行邏輯與我們生活中的非同步是一致的,首先發起了一個5秒的請求,但是程式不會卡住等待,而是繼續運行後面的程式碼,然後發起新的請求。由於新的請求返回時間短,所以新的請求很快返回並列印,最後才是列印的5秒請求的返回結果。

這就像是我們打開了洗衣機的電源,然後去淘米煮飯,米放進了電飯鍋,打開電飯鍋電源,然後去看書,最後飯先煮好,然後衣服再洗完。

JavaScript 原生的非同步請求的過程,與日常生活中的邏輯很像。所以很容易就能理解 JavaScript 的非同步流程。

但是 Python 裡面,非同步又是另外一種情況了。

我們來寫一段程式碼:

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秒請求返回:', 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())

運行效果如下圖所示:

可以看出,程式依然是串列運行的,根本就沒有非同步痕迹。

要讓程式非同步運行,我們需要湊夠一批任務提交給 asyncio,讓它自己通過事件循環來調度這些任務:

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秒請求返回:', 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())

運行效果如下圖所示:

這是由於,在asyncio 裡面,task是可以並行的最小單位,並且,task 要湊夠一批一起通過asyncio.gather或者asyncio.wait提交給事件循環以後,才能並行起來。

當使用程式碼asyncio.create_task(非同步函數())的時候,這個非同步函數實際上並沒有真正運行,所以,在上面的程式碼中:

tasks = [                  asyncio.create_task(test_delay(client)),                  asyncio.create_task(do_plus()),                  asyncio.create_task(test_ip(client)),                  asyncio.create_task(test_print())                  ]

創建了一個包含4個task 的列表,此時這4個非同步函數中的程式碼都還沒有執行。

當再調用await asyncio.gather(*tasks)時,這4個任務被作為4個參數傳入到了 asyncio.gather函數中,於是 Python 的事件循環開始調度他們。在這些非同步函數中,包含await的地方,就是在告訴 Python,await後面的這個函數可能會有 IO 等待,可以掛起等一會再來看,現在可以去檢查事件循環裡面其他非同步任務是否已經結束等待可以運行。而沒有 await的地方依然是串列的,例如do_plus裡面的三行程式碼就是按順序一次性運行完成的。

所以,當我們使用 Python 的 asyncio 寫非同步程式碼時,我們需要提前安排好非同步的切換位置並包裝為非同步任務,然後把一批任務一次性提交給 asyncio,讓 Python 自己根據我們安排好的切換邏輯來調度這些任務。

這就像是,當我寫 JavaScript 的時候,我親自上陣先把洗衣機電源打開,然後我再來考慮接下來要利用等待時間做什麼事情。

當我寫 Python 的時候,我需要提前把整個計劃都安排好:先打開洗衣機,在等待的時間淘米煮飯,然後再看書。並把這個計劃表提交給一個專門做事情的人來執行。

理解了這個差別,才能更好地在 Python 中使用 asyncio。