python协程系列(六)——EventLoop和Future详解以及concurrency实现

640?wx_fmt=gif

640?wx_fmt=jpeg

python进阶教程

机器学习

深度学习

长按二维码关注

进入正文


640?wx_fmt=png

python协程系列(六)——asyncio的EvenrLoop以及Future详解

声明:python协程系列文章的上一篇,即第五篇,详细介绍了asyncio的核心概念,asyncio的设计架构,Task类的详细作用,本文为系列文章的第六篇,将介绍更加底层的API,以EventLoop和Future为主,介绍他们的设计理念,包含的方法以及使用技巧。最后使用asyncio实现并发编程。本文较长,阅读全文大约30min。


目录

一 事件循环EventLoop

1.1 事件循环的创建、获取、设置

1.2 运行和停止事件循环

1.3 创建Future和Task

1.4 事件循环的时钟

1.5 计划执行回调函数(CallBacks)

二 底层API之Future

2.1 Future的定义概览

2.2 asyncio中关于Future的几个方法

2.3 Future对象的常用方法

三 集中回答以下几个问题

3.1 很多个协程一起运行有创建新的线程吗?

3.2 线程一定效率更高吗?

3.3 协程会不会有阻塞呢?

3.4 协程的4种状态

多任务实现并发

4.1 使用gather同时注册多个任务,实现并发

4.2 使用wait可以同时注册多个任务,实现并发

4.3 使用as_completed可以同时注册多个任务,实现并发

4.4 主调方获取任务的运行结果

下一篇预告

01

事件循环EventLoop

事件循环是asyncio的核心,异步任务的运行、任务完成之后的回调、网络IO操作、子进程的运行,都是通过事件循环完成的。在前一篇文章中,已经提到过,在python3.7中,我们甚至完全不用管事件循环,只需要使用高层API,即asyncio中的方法,我们很少直接与事件循环打交道,但是为了更加熟悉asyncio的运行原理,最好还是了解EventLoop的设计原理。


640?wx_fmt=png
事件循环的创建、获取、设置
640?wx_fmt=png

(1)asyncio.get_running_loop()。python3.7新添加的

(2)asyncio.get_event_loop()

(3)asyncio.set_event_loop(loop)

(4)asyncio.new_event_loop()


640?wx_fmt=png
运行和停止事件循环
640?wx_fmt=png

(1)loop.run_until_complete(future)。运行事件循环,直到future运行结束


640?wx_fmt=png
创建Future和Task
640?wx_fmt=png

(1)loop.create_future(coroutine) ,返回future对象


640?wx_fmt=png
事件循环的时钟

loop.time()。可以这么理解,事件循环内部也维护着一个时钟,可以查看事件循环现在运行的时间点是多少,就像普通的time.time()类似,它返回的是一个浮点数值,比如下面的代码。

import asyncio 

async def hello1(a,b):
    print('准备做加法运算')
    await asyncio.sleep(3)
    return a+b

loop=asyncio.get_event_loop()
t1=loop.time()  #开始时间
print(t1)
loop.run_until_complete(hello1(3,4))
t2=loop.time()  #结束时间
print(t2)
print(t2-t1)    #时间间隔
'''运行结果为:
28525.671
准备做加法运算
28528.703
3.0320000000028813
'''


640?wx_fmt=png
计划执行回调函数(CallBacks)
640?wx_fmt=png

(1)loop.call_later(delay, callback, *args, context=None)需要注意的是:上面的几个回调函数都只使用了“位置参数”哦,asyncio中,大部分的计划回调函数都不支持“关键字参数”,如果是想要使用关键字参数,则推荐使用functools.aprtial()对方法进一步包装,详细可以参考前面的python标准库系列文章。

# will schedule "print("Hello", flush=True)"
loop.call_soon(
    functools.partial(print, "Hello", flush=True))

下面来看一下具体的使用例子。

import asyncio

def callback(n):
    print('我是回调函数,参数为: {0} '.format(n))


async def main(loop):
    print('在异步函数中注册回调函数')
    loop.call_later(2, callback, 1)
    loop.call_later(1, callback, 2)
    loop.call_soon(callback, 3)

    await asyncio.sleep(4)


loop = asyncio.get_event_loop()
print('进入事件循环')
loop.run_until_complete(main(loop))
print('关闭事件循环')
loop.close()

'''运行结果为:
进入事件循环
在异步函数中注册回调函数
我是回调函数,参数为: 3
我是回调函数,参数为: 2
我是回调函数,参数为: 1
关闭事件循环
'''

再看一个简单的例子:

import asyncio

def callback(a, loop):
    print("我的参数为 {0},执行的时间为{1}".format(a,loop.time()))


#call_later, call_at
if __name__ == "__main__":
    try:
        loop = asyncio.get_event_loop()
        now = loop.time()
        loop.call_later(5, callback, 5, loop) #第一个参数设置的时间5.5秒后执行,
        loop.call_at(now+2, callback, 2, loop)    #在指定的时间,运行,当前时间+2秒
        loop.call_at(now+1, callback, 1, loop)
        loop.call_at(now+3, callback, 3, loop)
        loop.call_soon(callback, 4, loop)
        loop.run_forever()  #要用这个run_forever运行,因为没有传入协程,这个函数在3.7中已经被取消
    except KeyboardInterrupt:
        print("Goodbye!")

'''运行结果为:
我的参数为 4,执行的时间为266419.843
我的参数为 1,执行的时间为266420.843
我的参数为 2,执行的时间为266421.859
我的参数为 3,执行的时间为266422.859
我的参数为 5,执行的时间为266424.843
'''

总结注意事项:(4)如何理解?对于一般的异步函数,我们需要将它放在时间循环里面,然后通过事件循环去循环调用它,而因为CallBack并不是异步函数,它是定义为普通的同步方法,所以不能够放在时间循环里面,但是如果我依然想要让事件循环去执行它怎么办呢?那就不放进事件循环,直接让事件循环“立即、稍后、在什么时候”去执行它不就行了嘛,call的含义就是“执行”。


02

底层API之Future

640?wx_fmt=png
Future的定义概览
640?wx_fmt=png

Future的本质是一个类。他表示的是异步操作的最终将要返回的结果,故而命名为Future,它不是线程安全的。Future对象是awaitable的,参见系类文章的前面,

class asyncio.Future(*, loop=None)


640?wx_fmt=png
asyncio中关于Future的几个方法
640?wx_fmt=png

(1)asyncio.isfuture(obj) 。判断一个对象是不是Future,注意python中一切皆对象哦,包括函数,当obj是下面几种情况时返回true:concurrent.futures.Future对象包装成一个 asyncio.Future 对象。


640?wx_fmt=png
Future对象的常用方法
640?wx_fmt=png

(1)result()。返回Future执行的结果返回值(2)set_result(result)(3)set_exception(exception)(4)done()(5)cancelled()

(6)add_done_callback(callback, *, context=None)(7)remove_done_callback(callback)返回Future所绑定的事件循环


03

集中回答以下几个问题

通过前面的讲解,已经讲清楚了asyncio架构里面的一些基本东西,现在可以来集中回答以下一些常见的问题了,弄清楚这希尔问题,可以方便我们更加深入的理解协程。


640?wx_fmt=png
很多个协程一起运行有创建新的线程吗?
640?wx_fmt=png

协程运行时,都是在一个线程中运行的,没有创建新的线程。如下

import asyncio
import time
import threading

a=time.time()

async def hello1():
    print(f"Hello world 01 begin,my thread is:{threading.currentThread()}")
    await asyncio.sleep(3)
    print("Hello again 01 end")

async def hello2():
    print(f"Hello world 02 begin,my thread is:{threading.currentThread()}")
    await asyncio.sleep(2)
    print("Hello again 02 end")

async def hello3():
    print(f"Hello world 03 begin,my thread is:{threading.currentThread()}")
    await asyncio.sleep(1)
    print("Hello again 03 end")

loop = asyncio.get_event_loop()
tasks = [hello1(), hello2(),hello3()]
loop.run_until_complete(asyncio.wait(tasks))

loop.close()


b=time.time()
print('---------------------------------------')
print(b-a)
'''运行结果为:
Hello world 03 begin,my thread is:<_MainThread(MainThread, started 4168)>
Hello world 02 begin,my thread is:<_MainThread(MainThread, started 4168)>
Hello world 01 begin,my thread is:<_MainThread(MainThread, started 4168)>
Hello again 03 end
Hello again 02 end
Hello again 01 end
---------------------------------------
2.994506597518921
'''

从上面那个可以看出,三个不同的协程函数都是在一个线程完成的。但是并不是意味着,多个协程函数只能在一个线程中执行,同样可以创建新的线程,其实我们完全可以在新的线程中重新创建一个事件循环,具体的实例参见后面。


640?wx_fmt=png
线程一定效率更高吗?
640?wx_fmt=png

也不是绝对的,当然在一般情况下,异步方式的执行效率是更高的,就比如上面的三个函数,如果按照同步的方式执行,则一共需要6秒的时间,但是采用协程则只需要最长的那个时间3秒,这自然是提高了工作效率,那是不是一定会提高呢?也不一定,这与协程的调用方式是由密切关系的。如下所示:

import asyncio
import time
import threading

a=time.time()

async def hello1():
    print(f"Hello world 01 begin,my thread is:{threading.currentThread()}")
    await asyncio.sleep(3)
    print("Hello again 01 end")

async def hello2():
    print(f"Hello world 02 begin,my thread is:{threading.currentThread()}")
    await asyncio.sleep(2)
    print("Hello again 02 end")

async def hello3():
    print(f"Hello world 03 begin,my thread is:{threading.currentThread()}")
    await hello2()
    await hello1()
    print("Hello again 03 end")

loop = asyncio.get_event_loop()
tasks = [hello3()]
loop.run_until_complete(asyncio.wait(tasks))

loop.close()

b=time.time()
print('---------------------------------------')
print(b-a)

'''运行结果为:
Hello world 03 begin,my thread is:<_MainThread(MainThread, started 13308)>
Hello world 02 begin,my thread is:<_MainThread(MainThread, started 13308)>
Hello again 02 end
Hello world 01 begin,my thread is:<_MainThread(MainThread, started 13308)>
Hello again 01 end
Hello again 03 end
---------------------------------------
5.008373498916626
'''

我们发现一个问题,上面执行的顺序完全不是异步执行,执行的时间也没有得到改善,究其原因,是因为上面是通过hello3去调用hello1和hello2的,这和同步调用的方式完全是一样的,即使我定义的都是异步方法,它既没有提高执行效率,还会有阻塞。结论:在有很多个异步方式的时候,一定要尽量避免这种异步函数的直接调用,这和同步是没什么区别的,一定要通过事件循环loop,“让事件循环在各个异步函数之间不停游走”,这样才不会造成阻塞。


640?wx_fmt=png
协程会不会有阻塞呢?
640?wx_fmt=png

异步方式依然会有阻塞的,当我们定义的很多个异步方法彼此之间有一来的时候,比如,我必须要等到函数1执行完毕,函数2需要用到函数1的返回值,如上面的例子2所示,就会造成阻塞,这也是异步编程的难点之一,如何合理配置这些资源,尽量减少函数之间的明确依赖,这是很重要的。


640?wx_fmt=png
协程的4种状态
640?wx_fmt=png

协程函数相比于一般的函数来说,我们可以将协程包装成任务Task,任务Task就在于可以跟踪它的状态,我就知道它具体执行到哪一步了,一般来说,协程函数具有4种状态,可以通过相关的模块进行查看,请参见前面的文章,他的四种状态为:


04

多任务实现并发


python异步协程函数的最终目的是实现并发,这样才能提高工作效率。

我们经常看见下面这样的代码,即:

 tasks = asyncio.gather(*[task1,task2,task3])
 loop.run_until_complete(tasks)

#或者是
 tasks = asyncio.wait([task1,task2,task3])
 loop.run_until_complete(tasks)

#甚至可以写在一起,即
loop.run_until_complete(asyncio.gather(*[task1,task2,task3])
#或者是
asyncio.gather(asyncio.wait([task1,task2,task3]))

上面这些都是一些简单的应用,可以同时进行多任务,进行并发,但是如果我们每一个任务都有返回值,而且需要获取这些返回值,这样做显然还不够,还需要做进一步的处理。

asyncio实现并发的思想是一样的,只是实现的手段稍有区别,主要有以下几种实现方式:


640?wx_fmt=png
使用gather同时注册多个任务,实现并发
640?wx_fmt=png

awaitable asyncio.gather(*aws, loop=None, return_exceptions=False)

# import asyncio
# import time
# import threading

# a=time.time()

# async def hello1():
#     print(f"Hello world 01 begin,my thread is:{threading.currentThread()}")
#     await asyncio.sleep(3)
#     print("Hello again 01 end")

# async def hello2():
#     print(f"Hello world 02 begin,my thread is:{threading.currentThread()}")
#     await asyncio.sleep(2)
#     print("Hello again 02 end")

# async def hello3():
#     print(f"Hello world 03 begin,my thread is:{threading.currentThread()}")
#     await hello2()
#     await hello1()
#     print("Hello again 03 end")

# loop = asyncio.get_event_loop()
# tasks = [hello3()]
# loop.run_until_complete(asyncio.wait(tasks))

# loop.close()


# b=time.time()
# print('---------------------------------------')
# print(b-a)

import asyncio
import time


async def hello1(a,b):
    print("Hello world 01 begin")
    await asyncio.sleep(3)  #模拟耗时任务3秒
    print("Hello again 01 end")
    return a+b

async def hello2(a,b):
    print("Hello world 02 begin")
    await asyncio.sleep(2)   #模拟耗时任务2秒
    print("Hello again 02 end")
    return a-b

async def hello3(a,b):
    print("Hello world 03 begin")
    await asyncio.sleep(4)   #模拟耗时任务4秒
    print("Hello again 03 end")
    return a*b

async def main():  #封装多任务的入口函数
    task1=asyncio.ensure_future(hello1(10,5))
    task2=asyncio.ensure_future(hello2(10,5))
    task3=asyncio.ensure_future(hello3(10,5))
    results=await asyncio.gather(task1,task2,task3)   
    for result in results:    #通过迭代获取函数的结果,每一个元素就是相对应的任务的返回值,顺序都没变
        print(result)


loop = asyncio.get_event_loop()               
loop.run_until_complete(main())
loop.close()                                 

'''运行结果为:
Hello world 01 begin
Hello world 02 begin
Hello world 03 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
15
5
50
'''

640?wx_fmt=png
使用wait可以同时注册多个任务,实现并发
640?wx_fmt=png

await asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)

#前面的代码和上面一样
async def main():  #封装多任务的入口函数
    task1=asyncio.ensure_future(hello1(10,5))
    task2=asyncio.ensure_future(hello2(10,5))
    task3=asyncio.ensure_future(hello3(10,5))
    done,pending=await asyncio.wait([task1,task2,task3])   
    for done_task in done:
        print(done_task.result())  #这里返回的是一个任务,不是直接的返回值,故而需要使用result函数进行获取


loop = asyncio.get_event_loop()               
loop.run_until_complete(main())
loop.close()  

#运行结果也一样


640?wx_fmt=png
使用as_completed同时注册多个任务
             ,实现并发
640?wx_fmt=png

这个方法使用的比较少,与前面的两个gather和wait不同的是,它不是awaitable。使用实例参见前面的一篇文章,参见如下:


640?wx_fmt=png
主调方获取任务的运行结果
640?wx_fmt=png

上面的运行结果,都是在main()函数里面获取的运行结果,那可不可以不再main()里面获取结果呢,,当然是可以的,我们可以这样做,

async def main():  #封装多任务的入口函数
    task1=asyncio.ensure_future(hello1(10,5))
    task2=asyncio.ensure_future(hello2(10,5))
    task3=asyncio.ensure_future(hello3(10,5))

    return await asyncio.gather(task1,task2,task3)  #不在这里获取结果,只是返回


loop = asyncio.get_event_loop()               
results=loop.run_until_complete(main())  #在这里再获取返回函数值,然后迭代获取
for result in results:
    print(result)
loop.close()     

#y运行结果同上   

或者是如下:

async def main():  #封装多任务的入口函数
    task1=asyncio.ensure_future(hello1(10,5))
    task2=asyncio.ensure_future(hello2(10,5))
    task3=asyncio.ensure_future(hello3(10,5))

    return await asyncio.wait([task1,task2,task3])  #不在这里获取结果,只是返回


loop = asyncio.get_event_loop()               
done,pending=loop.run_until_complete(main())  #在这里再获取返回函数值,然后迭代获取
for done_task in done:
    print(done_task.result())
loop.close()

05

Future补充及下一篇预告


640?wx_fmt=png
Future补充
640?wx_fmt=png

asyncio中的Future类是模仿concurrent.futures.Future类而设计的,关于concurrent.futures.Future,可以查阅相关的文档。它们之间的主要区别是:

(1)asyncio.Future对象是awaitable的,但是concurrent.futures.Future对象是不能够awaitable的;

(2)asyncio.Future.result()和asyncio.Future.exception()是不接受关键字参数timeout的;

(3)当Future没有完成的时候,asyncio.Future.result()和asyncio.Future.exception()将会触发一个InvalidStateError异常;

(4)使用asyncio.Future.add_done_callback()注册的回调函数不会立即执行,它可以使用loop.call_soon代替;

(5)asyncio里面的Future和concurrent.futures.wait()以及concurrent.futures.as_completed()是不兼容的。

有兴趣的小伙伴可以自己学一下concurrent.futures哦!


640?wx_fmt=png
下一篇预告
640?wx_fmt=png

多线程+asyncio协程。实现更加强健的程序。


推 荐 阅 读

640?wx_fmt=jpeg

赶紧关注我们吧

您的点赞和分享是我们进步的动力!

↘↘↘

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值