创建task后,task在加入事件循环之前是pending状态,因为do_some_work中没有耗时的阻塞操作,task很快就执行完毕了。后面打印的finished状态。
asyncio.ensure_future(coroutine) 和 loop.create_task(coroutine)都可以创建一个task,run_until_complete的参数是一个futrue对象。当传入一个协程,其内部会自动封装成task,task是Future的子类。isinstance(task, asyncio.Future)将会输出True。
绑定回调
绑定回调,在task执行完毕的时候可以获取执行的结果,回调的最后一个参数是future对象,通过该对象可以获取协程返回值。如果回调需要多个参数,可以通过偏函数导入。
importtimeimportasyncio
now= lambda: time.time()
asyncdefdo_some_work(x):print('Waiting:', x)return 'Done after {}s'.format(x)defcallback(future):print('Callback:', future.result())
start=now()
coroutine= do_some_work(2)
loop=asyncio.get_event_loop()
task=asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
loop.run_until_complete(task)print('TIME:', now() - start)
利用偏函数
defcallback(t, future):print('Callback:', t, future.result())
task.add_done_callback(functools.partial(callback,2))
可以看到,coroutine执行结束时候会调用回调函数。并通过参数future获取协程执行的结果。我们创建的task和回调里的future对象,实际上是同一个对象。
future 与 result
回调一直是很多异步编程的恶梦,程序员更喜欢使用同步的编写方式写异步代码,以避免回调的恶梦。回调中我们使用了future对象的result方法。前面不绑定回调的例子中,我们可以看到task有fiinished状态。在那个时候,可以直接读取task的result方法。
async defdo_some_work(x):print('Waiting {}'.format(x))return 'Done after {}s'.format(x)
start=now()
coroutine= do_some_work(2)
loop=asyncio.get_event_loop()
task=asyncio.ensure_future(coroutine)
loop.run_until_complete(task)print('Task ret: {}'.format(task.result()))print('TIME: {}'.format(now() - start))
可以看到输出的结果:
Waiting: 2Task ret: Done after 2s
TIME:0.0003650188446044922
阻塞和await
使用async可以定义协程对象,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。协程遇到await,事件循环将会挂起该协程,执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。
耗时的操作一般是一些IO操作,例如网络请求,文件读取等。我们使用asyncio.sleep函数来模拟IO操作。协程的目的也是让这些IO操作异步化。
importasyncioimporttime
now= lambda: time.time()
asyncdefdo_some_work(x):print('Waiting:', x)
await asyncio.sleep(x)return 'Done after {}s'.format(x)
start=now()
coroutine= do_some_work(2)
loop=asyncio.get_event_loop()
task=asyncio.ensure_future(coroutine)
loop.run_until_complete(task)print('Task ret:', task.result())print('TIME:', now() - start)
在 sleep的时候,使用await让出控制权。即当遇到阻塞调用的函数的时候,使用await方法将协程的控制权让出,以便loop调用其他的协程。现在我们的例子就用耗时的阻塞操作了。
并发和并行
并发和并行一直是容易混淆的概念。并发通常指有多个任务需要同时进行,并行则是同一时刻有多个任务执行。用上课来举例就是,并发情况下是一个老师在同一时间段辅助不同的人功课。并行则是好几个老师分别同时辅助多个学生功课。简而言之就是一个人同时吃三个馒头还是三个人同时分别吃一个的情况,吃一个馒头算一个任务。
asyncio实现并发,就需要多个协程来完成任务,每当有任务阻塞的时候就await,然后其他协程继续工作。创建多个协程的列表,然后将这些协程注册到事件循环中。
importasyncioimporttime
now= lambda: time.time()
asyncdefdo_some_work(x):print('Waiting:', x)
await asyncio.sleep(x)return 'Done after {}s'.format(x)
start=now()
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)
]
loop=asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))for task intasks:print('Task ret:', task.result())print('TIME:', now() - start)
结果如下
Waiting: 1Waiting:2Waiting:4Task ret: Done after 1s
Task ret: Done after 2s
Task ret: Done after 4s
TIME:4.003541946411133
协程嵌套
使用async可以定义协程,协程用于耗时的io操作,我们也可以封装更多的io操作过程,这样就实现了嵌套的协程,即一个协程中await了另外一个协程,如此连接起来。
importasyncioimporttime
now= lambda: time.time()
asyncdefdo_some_work(x):print('Waiting:', x)
await asyncio.sleep(x)return 'Done after {}s'.format(x)
asyncdefmain():
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)
]
dones, pendings=await asyncio.wait(tasks)for task indones:print('Task ret:', task.result())
start=now()
loop=asyncio.get_event_loop()
loop.run_until_complete(main())print('TIME:', now() - start)
如果使用的是 asyncio.gather创建协程对象,那么await的返回值就是协程运行的结果。
results = await asyncio.gather(*tasks)for result inresults:print('Task ret:', result)
不在main协程函数里处理结果,直接返回await的内容,那么最外层的run_until_complete将会返回main协程的结果。
async defmain():
coroutine1= do_some_work(1)
coroutine2= do_some_work(2)
coroutine3= do_some_work(2)
tasks=[
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]return await asyncio.gather(*tasks)
start=now()
loop=asyncio.get_event_loop()
results=loop.run_until_complete(main())for result inresults:print('Task ret:', result)
或者返回使用asyncio.wait方式挂起协程。
async defmain():
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)
]returnawait asyncio.wait(tasks)
start=now()
loop=asyncio.get_event_loop()
done, pending=loop.run_until_complete(main())for task indone:print('Task ret:', task.result())
也可以使用asyncio的as_completed方法
async defmain():
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)
]for task inasyncio.as_completed(tasks):
result=await taskprint('Task ret: {}'.format(result))
start=now()
loop=asyncio.get_event_loop()
done=loop.run_until_complete(main())print('TIME:', now() - start)
协程停止
上面见识了协程的几种常用的用法,都是协程围绕着事件循环进行的操作。future对象有几个状态:
Pending
Running
Done
Cancelled
创建future的时候,task为pending,事件循环调用执行的时候当然就是running,调用完毕自然就是done,如果需要停止事件循环,就需要先把task取消。可以使用asyncio.Task获取事件循环的task
importasyncioimporttime
now= lambda: time.time()
asyncdefdo_some_work(x):print('Waiting:', x)
await asyncio.sleep(x)return 'Done after {}s'.format(x)
coroutine1= do_some_work(1)
coroutine2= do_some_work(2)
coroutine3= do_some_work(2)
tasks=[
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]
start=now()
loop=asyncio.get_event_loop()try:
loop.run_until_complete(asyncio.wait(tasks))exceptKeyboardInterrupt as e:print(asyncio.Task.all_tasks())for task inasyncio.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: 1Waiting:2Waiting:2{ wait_for=()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, wait_for=()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, wait_for=()]> cb=[_run_until_complete_cb() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py:176]>, wait_for=()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>}
True
True
True
True
TIME:0.8858370780944824
True表示cannel成功,loop stop之后还需要再次开启事件循环,最后在close,不然还会抛出异常:
Task was destroyed but it ispending!
task:
循环task,逐个cancel是一种方案,可是正如上面我们把task的列表封装在main函数中,main函数外进行事件循环的调用。这个时候,main相当于最外出的一个task,那么处理包装的main函数即可。
importasyncioimporttime
now= lambda: time.time()
asyncdefdo_some_work(x):print('Waiting:', x)
await asyncio.sleep(x)return 'Done after {}s'.format(x)
asyncdefmain():
coroutine1= do_some_work(1)
coroutine2= do_some_work(2)
coroutine3= do_some_work(2)
tasks=[
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]
done, pending=await asyncio.wait(tasks)for task indone:print('Task ret:', task.result())
start=now()
loop=asyncio.get_event_loop()
task=asyncio.ensure_future(main())try:
loop.run_until_complete(task)exceptKeyboardInterrupt as e:print(asyncio.Task.all_tasks())print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
loop.stop()
loop.run_forever()finally:
loop.close()
不同线程的事件循环
很多时候,我们的事件循环用于注册协程,而有的协程需要动态的添加到事件循环中。一个简单的方式就是使用多线程。当前线程创建一个事件循环,然后在新建一个线程,在新线程中启动事件循环。当前线程不会被block。
from threading importThreaddefstart_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()defmore_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))
new_loop.call_soon_threadsafe(more_work,6)
new_loop.call_soon_threadsafe(more_work,3)
启动上述代码之后,当前线程不会被block,新线程中会按照顺序执行call_soon_threadsafe方法注册的more_work方法,后者因为time.sleep操作是同步阻塞的,因此运行完毕more_work需要大致6 + 3
新线程协程
defstart_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
asyncdefdo_some_work(x):print('Waiting {}'.format(x))
await asyncio.sleep(x)print('Done after {}s'.format(x))defmore_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左右。
master-worker主从模式
对于并发任务,通常是用生成消费模型,对队列的处理可以使用类似master-worker的方式,master主要用户获取队列的msg,worker用户处理消息。
为了简单起见,并且协程更适合单线程的方式,我们的主线程用来监听队列,子线程用于处理队列。这里使用redis的队列。主线程中有一个是无限循环,用户消费队列。
whileTrue:
task= rcon.rpop("queue")if nottask:
time.sleep(1)continueasyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
给队列添加一些数据:
127.0.0.1:6379[3]> lpush queue 2(integer)1
127.0.0.1:6379[3]> lpush queue 5(integer)1
127.0.0.1:6379[3]> lpush queue 1(integer)1
127.0.0.1:6379[3]> lpush queue 1
可以看见输出:
Waiting 2Done2Waiting5Waiting1Done1Waiting1Done1Done5
我们发起了一个耗时5s的操作,然后又发起了连个1s的操作,可以看见子线程并发的执行了这几个任务,其中5s awati的时候,相继执行了1s的两个任务。
停止子线程
如果一切正常,那么上面的例子很完美。可是,需要停止程序,直接ctrl+c,会抛出KeyboardInterrupt错误,我们修改一下主循环:
try:whileTrue:
task= rcon.rpop("queue")if nottask:
time.sleep(1)continueasyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)exceptKeyboardInterrupt as e:print(e)
new_loop.stop()
可是实际上并不好使,虽然主线程try了KeyboardInterrupt异常,但是子线程并没有退出,为了解决这个问题,可以设置子线程为守护线程,这样当主线程结束的时候,子线程也随机退出。
new_loop =asyncio.new_event_loop()
t= Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True)#设置子线程为守护线程
t.start()try:whileTrue:#print('start rpop')
task = rcon.rpop("queue")if nottask:
time.sleep(1)continueasyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)exceptKeyboardInterrupt as e:print(e)
new_loop.stop()
线程停止程序的时候,主线程退出后,子线程也随机退出才了,并且停止了子线程的协程任务。
aiohttp
在消费队列的时候,我们使用asyncio的sleep用于模拟耗时的io操作。以前有一个短信服务,需要在协程中请求远程的短信api,此时需要是需要使用aiohttp进行异步的http请求。大致代码如下:
server.py
importtimefrom flask importFlask, request
app= Flask(__name__)
@app.route('/')defindex(x):
time.sleep(x)return "{} It works".format(x)
@app.route('/error')deferror():
time.sleep(3)return "error!"
if __name__ == '__main__':
app.run(debug=True)
/接口表示短信接口,/error表示请求/失败之后的报警。
async-custoimer.py
importtimeimportasynciofrom threading importThreadimportredisimportaiohttpdefget_redis():
connection_pool= redis.ConnectionPool(host='127.0.0.1', db=3)return redis.Redis(connection_pool=connection_pool)
rcon=get_redis()defstart_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
asyncdeffetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:print(resp.status)returnawait resp.text()
asyncdefdo_some_work(x):print('Waiting', x)try:
ret= await fetch(url='http://127.0.0.1:5000/{}'.format(x))print(ret)exceptException as e:try:print(await fetch(url='http://127.0.0.1:5000/error'))exceptException as e:print(e)else:print('Done {}'.format(x))
new_loop=asyncio.new_event_loop()
t= Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True)
t.start()try:whileTrue:
task= rcon.rpop("queue")if nottask:
time.sleep(1)continueasyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)exceptException as e:print('error')
new_loop.stop()finally:pass
对于redis的消费,还有一个block的方法:
try:whileTrue:
_, task= rcon.brpop("queue")
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)exceptException as e:print('error', e)
new_loop.stop()finally:pass
使用 brpop方法,会block住task,如果主线程有消息,才会消费。测试了一下,似乎brpop的方式更适合这种队列消费的模型。
127.0.0.1:6379[3]> lpush queue 5(integer)1
127.0.0.1:6379[3]> lpush queue 1(integer)1
127.0.0.1:6379[3]> lpush queue 1
可以看到结果
Waiting 5Waiting1Waiting1
200
1It works
Done1
200
1It works
Done1
200
5It works
Done5
协程消费
主线程用于监听队列,然后子线程的做事件循环的worker是一种方式。还有一种方式实现这种类似master-worker的方案。即把监听队列的无限循环逻辑一道协程中。程序初始化就创建若干个协程,实现类似并行的效果。
importtimeimportasyncioimportredis
now= lambda: time.time()defget_redis():
connection_pool= redis.ConnectionPool(host='127.0.0.1', db=3)return redis.Redis(connection_pool=connection_pool)
rcon=get_redis()
asyncdefworker():print('Start worker')whileTrue:
start=now()
task= rcon.rpop("queue")if nottask:
await asyncio.sleep(1)continue
print('Wait', int(task))
await asyncio.sleep(int(task))print('Done', task, now() -start)defmain():
asyncio.ensure_future(worker())
asyncio.ensure_future(worker())
loop=asyncio.get_event_loop()try:
loop.run_forever()exceptKeyboardInterrupt as e:print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
loop.stop()
loop.run_forever()finally:
loop.close()if __name__ == '__main__':
main()