python中的asyncio使用詳解

在python的異步實踐中,每次看asynicio都會有新的收穫,本篇總結一下最近看這個庫的使用。

一些核心概念

異步函數的定義

普通函數的定義是使用 def 關鍵詞,異步的函數,協程函數(Coroutine)本質上是一個函數,特點是在代碼塊中可以將執行權交給其他協程,使用async def 來定義

1
2
3
4
5
6
7
8
9
# 普通函數定義
def add2(x):
    print(x+2)
    return x+2

# 異步函數的定義
async def add3(x):
    print("in async fun add")
    return x+3

如何調用協程並且得到它的運行結果?
調用普通的函數只需要 result = add2(2),這時函數就可以得到運行,並且將結果4返回給result,如果使用result = add3(2),此時再打印 result 呢?
得到的是一個coroutine對象,<coroutine object add3 at 0x000002ED564A5048>,並不是2+3=5這個結果,怎樣才能得到結果呢?
協程函數想要執行需要放到事件循環裏執行。

事件循環 Eventloop

Eventloop 是asyncio應用的核心,把一些異步函數註冊到這個事件循環上,事件循環會循環執行這些函數,當執行到某個函數時,如果它正在等待I/O返回,如它正在進行網絡請求,或者sleep操作,事件循環會暫停它的執行去執行其他的函數;當某個函數完成I/O後會恢復,下次循環到它的時候繼續執行。因此,這些異步函數可以協同(Cooperative)運行:這就是事件循環的目標。

返回到上面的函數,想要得到函數執行結果,需要有一個Eventloop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio

loop = asyncio.get_event_loop()

async def add3(x):
    print("in async fun add")
    return x+3

result = loop.run_until_complete(add3(2))
print(result)

#運行的結果是
#in async fun add
#5

 

或者使用await 關鍵字來修飾函數的調用,如result = await add3(2),但是await只能用在協程函數中,所以想要用await關鍵字就還需要定義一個協程函數

1
2
3
4
5
6
7
async def add3(x):
    print("in async fun add")
    return x+3

async def main():
    result = await add3(2)
    return result

 

但最終的執行還是需要放到一個事件循環中進行.

稍微複雜一點的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# coding:utf-8

import asyncio
import time


async def testa(x):
    print("in test a")
    await asyncio.sleep(3)
    print("Resuming a")
    return x


async def testb(x):
    print("in test b")
    await asyncio.sleep(1)
    print('Resuming b')
    return x


async def main():
    start = time.time()
    resulta = await testa(1)
    resultb = await testb(2)
    print("test a result is %d"%resulta)
    print("test b result is %d"%resultb)
    print("use %s time"%(time.time()-start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

 

這段代碼定義了兩個協程,並將它們放到另外一個協程main函數中,想要獲得它們運行的結果,事件循環的特點是當它遇到某個I/O需要等待(如這裏的asyncio.sleep()函數)的時候,可以去執行其它的函數,這樣,整個函數執行所需要的時間,應該是所有協程中執行時間最長的那個,對於上面這個代碼來說,一個sleep了3秒,一個sleep了1秒,總的用時應該是3秒多一點,但結果是這樣嗎?

它的輸出是這樣的

1
2
3
4
5
6
7
in test a
Resuming a
in test b
Resuming b
test a result is 1
test b result is 2
use 4.001966714859009 time

 

它的用時是4秒多一點,而且是先執行了testa函數,然後再執行了testb函數,是串行的依次執行的,並沒有像我們想象中的併發執行。那應該怎樣才能併發執行呢?

1
2
3
4
5
6
7
8
9
10
11
async def main():
    start = time.time()
    resulta,resultb = await asyncio.gather(testa(1),testb(2))
    print("test a result is %d" % resulta)
    print("test b result is %d" % resultb)
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

 

需要將協程放到asyncio.gather() 中運行,上面的代碼得到的輸出是

1
2
3
4
5
6
7
in test b
in test a
Resuming b
Resuming a
test a result is 1
test b result is 2
use 3.001237392425537 time

 

可以看到,testa和testb是同步在運行,由於testb只sleep了1秒鐘,所以testb先輸出了Resuming b,最後將每個協程函數的結果返回,注意,這裏是gather()函數裏的每一個協程函數都執行完了,它才結果,結果是一個列表,列表裏的值順序和放到gather函數裏的協程的順序是一致的。

除了使用asyncio.gather 來執行協程函數以外,還可以使用Task任務對象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async def main():
    start = time.time()

    taska = asyncio.ensure_future(testa(1))
    taskb = asyncio.ensure_future(testb(2))

    print(taska)
    print(taskb)
    print(taska.done(), taskb.done())
    await taskb
    await taska
    print(taska.done(), taskb.done())

    print(taskb.result())
    print(taska.result())
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

 

使用asyncio.ensure_future(testa(1)) 返回一個task對象,此時task進入pending狀態,並沒有執行,這時print(taska) 得到<Task pending coro=<testa() running at F:/python/python3Test/asynctest.py:7>>些時,taska.done()返回False,表示它還沒有結束,當調用await taska 時表示開始執行該協程,當執行結束以後,taska.done() 返回True,這時可以調用taska.result() 得到函數的返回值,如果協程還沒有結束就調用result()方法則會拋個異常,raise InvalidStateError('Result is not ready.').

創建task對象除了使用asyncio.ensure_future()方法還可以使用loop.create_task() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async def main():
    start = time.time()

    taska = loop.create_task(testa(1))
    taskb = loop.create_task(testb(2))

    print(taska)
    print(taskb)
    print(taska.done(), taskb.done())
    await taskb
    await taska
    print(taska.done(), taskb.done())

    print(taskb.result())
    print(taska.result())
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

 

上面一直在使用asyncio.gather()函數來執行協程函數,還有一個asyncio.wait()函數,它的參數是協程的列表。

1
2
3
4
5
6
7
8
9
10
11
12
async def main():
    start = time.time()
    done,pending = await asyncio.wait([testa(1),testb(2)])
    print(list(done))
    print(list(pending))
    print(list(done)[0].result())
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

 

asyncio.wait() 返回一個tuple對象,對象裏又包含一個已經完成的任務set和未完成任務的set,上面代碼得到的結果是

1
2
3
4
5
6
7
8
in test b
in test a
Resuming b
Resuming a
[<Task finished coro=<testa() done, defined at F:/python/python3Test/asynctest.py:7> result=1>, <Task finished coro=<testb() done, defined at F:/python/python3Test/asynctest.py:14> result=2>]
[]
1
use 3.0003058910369873 time

 

使用wait和gather有哪些區別呢?
首先,gather是需要所有任務都執行結束,如果某一個協程函數崩潰了,則會拋異常,都不會有結果。
wait可以定義函數返回的時機,可以是FIRST_COMPLETED(第一個結束的), FIRST_EXCEPTION(第一個出現異常的), ALL_COMPLETED(全部執行完,默認的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# coding:utf-8

import asyncio
import time


async def testa(x):
    print("in test a")

    await asyncio.sleep(3)
    print(1/0)
    print("Resuming a")
    return x


async def testb(x):
    print("in test b")
    await asyncio.sleep(1)
    print(1/0)
    print('Resuming b')
    return x


async def main():
    start = time.time()
    done,pending = await asyncio.wait([testa(1),testb(2)],return_when=asyncio.tasks.FIRST_EXCEPTION)
    print(list(done))
    print(list(pending))
    print("use %s time" % (time.time() - start))

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

 

這段代碼要求在出現第一個異常的時候就結果,函數整體不會崩潰,只是如果這裏想要獲取結果的話它是一個異常對象。

1
2
3
4
5
in test b
in test a
[<Task finished coro=<testb() done, defined at F:/python/python3Test/asynctest.py:16> exception=ZeroDivisionError('division by zero',)>]
[<Task pending coro=<testa() running at F:/python/python3Test/asynctest.py:10> wait_for=<Future pending cb=[Task._wakeup()]>>]
use 1.0000195503234863 time

 

這篇文章介紹了一些asyncio的基本使用,之後會對Future和Task類做更深入的學習。

參考文章
深入理解asyncio(一)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章