asyncio模块
asyncio模块
asyncio 可以用来搞异步网络请求,并发,协程。
协程通过 async/await 语法进行声明,是编写异步应用的推荐方式。
python3.0时代,标准库里的异步网络模块:select(非常底层) python3.0时代,第三方异步网络库:Tornado python3.4时代,asyncio:支持TCP,子进程
现在的asyncio,有了很多的模块已经在支持:aiohttp,aiodns,aioredis等等 https://github.com/aio-libs 这里列出了已经支持的内容,并在持续更新
当然到目前为止实现协程的不仅仅只有asyncio,tornado和gevent都实现了类似功能
关键字
- async/await , async用来定义一个协程,await用于挂起阻塞的异步调用接口
- event_loop ,事件循环,程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数
- coroutine 协程,协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用
- task 任务,一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态
- future ,代表将来执行或没有执行的任务的结果。它和task上没有本质上的区别
定义一个协程
demo
import time
import asyncio
now = lambda: time.time()
async def do_some_work(x):
print("waiting:", x)
start = now()
# 这里是一个协程对象,这个时候do_some_work函数并没有执行
coroutine = do_some_work(2)
print(coroutine)
# 创建一个事件loop
loop = asyncio.get_event_loop()
# 将协程加入到事件循环loop
loop.run_until_complete(coroutine)
print("Time:", now() - start)
多个协程 demo1
import asyncio
async def func(i):
print('start {}th'.format(i))
await asyncio.sleep(1)
print('finish {}th'.format(i))
loop = asyncio.get_event_loop()
coroutine_list = (func(i) for i in range(10))
loop.run_until_complete(asyncio.gather(*coroutine_list))
多个协程 demo2
import asyncio
async def func(i):
print('start {}th'.format(i))
await asyncio.sleep(1)
print('finish {}th'.format(i))
loop = asyncio.get_event_loop()
task_list = [asyncio.ensure_future(func(i)) for i in range(10)]
loop.run_until_complete(asyncio.wait(task_list))
# loop.run_until_complete(asyncio.gather(*task_list)) 同样效果
创建一个task
协程对象不能直接运行,在注册事件循环的时候,其实是run_until_complete方法将协程包装成为了一个任务(task)对象. task对象是Future类的子类,保存了协程运行后的状态,用于未来获取协程的结果
import asyncio
import time
now = lambda: time.time()
async def do_some_work(x):
print("waiting:", x)
start = now()
coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine) # 3.7新功能
# task = asyncio.ensure_future(coroutine) # 没有loop情况下创建 task
print(task)
loop.run_until_complete(task)
print(task)
print("Time:", now() - start)
绑定回调
绑定回调,在task执行完成的时候可以获取执行的结果,回调的最后一个参数是future对象,通过该对象可以获取协程返回值。
通过add_done_callback方法给task任务添加回调函数,当task(也可以说是coroutine)执行完成的时候,就会调用回调函数。并通过参数future获取协程执行的结果。这里我们创建 的task和回调里的future对象实际上是同一个对象
import time
import asyncio
now = lambda: time.time()
async def do_some_work(x):
print("waiting:", x)
return "Done after {}s".format(x)
def callback(future):
print("callback:", future.result())
start = now()
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(do_some_work(2))
print(task)
task.add_done_callback(callback)
print(task)
loop.run_until_complete(task)
print("Time:", now() - start)
阻塞和await
使用async可以定义协程对象,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。协程遇到await,事件循环将会挂起该协程,执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行
耗时的操作一般是一些IO操作,例如网络请求,文件读取等。我们使用asyncio.sleep函数来模拟IO操作。协程的目的也是让这些IO操作异步化。
import asyncio
import time
import random
now = lambda: time.time()
async def do_some_work(x):
print("waiting:", x)
# await 后面就是调用耗时的操作
await asyncio.sleep(x)
return "Done after {}s".format(x)
start = now()
tl = [asyncio.ensure_future(do_some_work(random.randint(0, 5))) for i in range(10)] # task list
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tl))
print("Time:", now() - start) # Time: 5.0044331550598145
不在main协程函数里处理结果,直接返回await的内容,那么最外层的run_until_complete将会返回main协程的结果。 将上述的代码更改为:
import asyncio
import time
now = lambda: time.time()
async def do_some_work(x):
print("waiting:", x)
await asyncio.sleep(x)
return "Done after {}s".format(x)
async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)
tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]
# return await asyncio.gather(*tasks) # gather方式
return await asyncio.wait(tasks) # wait方式
start = now()
loop = asyncio.get_event_loop()
# results = loop.run_until_complete(main()) # gather 方式
# for result in results:
# print("Task ret:", result)
done, pending = loop.run_until_complete(main()) # wait方式
for task in done:
print("Task ret:", task.result())
print("Time:", now() - start)
可等待对象 await
可等待对象,await,如果一个对象可以在 await 语句中使用,那么它就是 可等待 对象。许多 asyncio API 都被设计为接受可等待对象。
可等待 对象有三种主要类型: 协程, 任务 和 Future.
- 协程定义: async def
- 任务 task ,通过 asyncio.create_task() 打包为一个 task
- Future 对象,表示一个异步操作的 最终结果。
协程停止
import asyncio
import time
import random
now = lambda: time.time()
async def do_some_work(x):
print("Waiting:", x)
await asyncio.sleep(x)
return "Done after {}s".format(x)
tasks = [asyncio.ensure_future(do_some_work(random.randint(0, 3))) for i in range(3)]
start = now()
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
print(asyncio.Task.all_tasks())
for task in asyncio.Task.all_tasks():
print(task.cancel())
loop.stop()
loop.run_forever()
finally:
loop.close()
print("Time:", now() - start)
启动事件循环之后,马上ctrl+c,会触发run_until_complete的执行异常 KeyBorardInterrupt。然后通过循环asyncio.Task取消future。可以看到输出如下:
Waiting: 2
Waiting: 0
Waiting: 1
{<Task finished coro=<do_some_work() done, defined at test.py:8>
result='Done after 0s'>, <Task pending coro=<do_some_work() running at test.py:10> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x0000019106B23A68>()]> cb=[_wait.<
locals>._on_completion() at C:\Users\**\AppData\Local\Programs\Python\Python37\lib\asyncio\tasks.py:440]>, <Task p
ending coro=<wait() running at C:\Users\**\AppData\Local\Programs\Python\Python37\lib\asyncio\tasks.py:363> wait_f
or=<Future pending cb=[<TaskWakeupMethWrapper object at 0x0000019106B23B88>()]>>, <Task pending coro=<do_some_work() r
unning at test.py:10> wait_for=<Future pending cb=[<TaskWakeupMe
thWrapper object at 0x0000019106AAF7F8>()]> cb=[_wait.<locals>._on_completion() at C:\Users\**\AppData\Local\Progr
ams\Python\Python37\lib\asyncio\tasks.py:440]>}
False
True
True
True
Time: 1.001704216003418
True表示cannel成功,loop stop之后还需要再次开启事件循环,最后在close,不然还会抛出异常
循环task,逐个cancel是一种方案,可是正如上面我们把task的列表封装在main函数中,main函数外进行事件循环的调用。这个时候,main相当于最外出的一个task,那么处理包装的main函数即可。
不同线程的事件循环
很多时候,我们的事件循环用于注册协程,而有的协程需要动态的添加到事件循环中。一个简单的方式就是使用多线程。当前线程创建一个事件循环,然后在新建一个线程,在新线程中启动事件循环。当前线程不会被block。
import asyncio
from threading import Thread
import time
now = lambda: time.time()
def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
def more_work(x):
print('More work {}'.format(x))
time.sleep(x)
print('Finished more work {}'.format(x))
start = now()
loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(loop,))
t.start()
print('TIME: {}'.format(time.time() - start))
loop.call_soon_threadsafe(more_work, 6)
loop.call_soon_threadsafe(more_work, 3)
启动上述代码之后,当前线程不会被block,新线程中会按照顺序执行call_soon_threadsafe方法注册的more_work方法, 后者因为time.sleep操作是同步阻塞的,因此运行完毕more_work需要大致6 + 3
非阻塞版本
import asyncio
import time
from threading import Thread
now = lambda: time.time()
def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
async def do_some_work(x):
print('Waiting {}'.format(x))
await asyncio.sleep(x)
print('Done after {}s'.format(x))
def more_work(x):
print('More work {}'.format(x))
time.sleep(x)
print('Finished more work {}'.format(x))
start = now()
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print('TIME: {}'.format(time.time() - start))
asyncio.run_coroutine_threadsafe(do_some_work(6), new_loop)
asyncio.run_coroutine_threadsafe(do_some_work(4), new_loop)
上述的例子,主线程中创建一个new_loop,然后在另外的子线程中开启一个无限事件循环。 主线程通过run_coroutine_threadsafe新注册协程对象。这样就能在子线程中进行事件循环的并发操作,同时主线程又不会被block。一共执行的时间大概在6s左右。
关于同步、异步、阻塞、非阻塞的理解
“阻塞”与"非阻塞"与"同步"与“异步"不能简单的从字面理解,提供一个从分布式系统角度的回答。
1.同步与异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。而异步则是相反,调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。
典型的异步编程模型比如Node.js
举个通俗的例子:
你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。\2. 阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。还是上面的例子,
你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。如果是关心阻塞 IO/ 异步 IO, 参考 Unix Network Programming View Book
--
还是2014年写的以解释概念为主,主要是同步异步 阻塞和非阻塞会被用在不同层面上,可能会有不准确的地方,并没有针对 阻塞 IO/ 异步 IO 等进行讨论,大家可以后续看看这两个回答: