一 基本概念

协程(Coroutine),是一种用户态的轻量级线程,又称微线程,纤程,可以实现单线程下的并发。是一种用户态内进行上下文切换的技术,由用户程序自己控制任务调度的,简而言之,其实就是通过线程可以实现代码块相互切换执行。协程与线程、进程同属于python中实现异步多任务的常用方式。

CPU能识别的最小任务调度单位是线程,而对于CPU来说,协程是不被识别的或不可见的。

从单进程到多进程提高了 CPU 利用率;从进程到线程,降低了上下文切换的开销;而从线程到协程,进一步降低了上下文切换的开销,使得高并发的服务可以使用简单的代码写出来的。

协程实现了一个线程内多个任务交替执行

在Python中有2种方式可以实现协程:

  1. python原生语法实现:生成器(yield & yield from) ----> async & await ---> asyncio(底层)

  2. C语言底层模块显示:greenlet ----> gevent / eventlet

 

二 基于生成器实现协程【少用】

普通函数中使用了yield关键字以后,该函数就会变成生成器函数

 1 # 在普通函数中使用了yield关键字以后,该函数就会变成生成器函数
 2 
 3 def func1():
 4     print("1-1. func1任务执行了")
 5     yield from func2()  # 交出CPU的执行权,去迭代执行 func2生成器函数(CPU资源的让渡),
 6     print("1-2. func1任务结束了")
 7 
 8 def func2():
 9     print("2-1. func1任务执行了")
10     yield   # 也进行了交出CPU的执行权(让渡)
11     print("2-2. func1任务结束了")
12 
13 if __name__ == '__main__':
14     # print(func1())  # 生成器函数的返回值是一个生成器对象
15     for item in func1():
16         item
17 
18 # 实现了一个线程内多个任务交替执行,这就是协程 !!!
19 
20 '''
21 1-1. func1任务执行了
22 2-1. func1任务执行了
23 2-2. func1任务结束了
24 1-2. func1任务结束了
25 '''
基于生成器实现协议

 

三 基于greenlet模块实现协程【少用】

 1 import time
 2 
 3 from greenlet import greenlet
 4 
 5 
 6 def func1():
 7     print("1-1. func1任务执行了")   # 第2步:输出 1-1
 8     g2.switch(2)                  # 第3步:调度切换,调度执行func2,并把参数2传递到任务中
 9     print("1-2. func1任务结束了")   # 第7步:输出 1-2
10 
11 def func2(n):
12     print(f"{n}-1. func1任务执行了")  # 第4步,输出n-1
13     print(f"{n}-2. func1任务结束了")  # 第5步,输出n-2
14     g1.switch()                     # 第6步,调度切换,调度执行func1,恢复func1的执行状态
15 
16 if __name__ == '__main__':
17     # 创建2个协程,参数就是协程要执行的任务
18     g1 = greenlet(func1)
19     g2 = greenlet(func2)
20     g1.switch()     # 第1步,切换协程,并且也可以传递参数到协程任务中
21     print("主程序")  # 第8步,因为不再有协程需要执行了,所以主程序结束
22 
23 '''
24 1-1. func1任务执行了
25 2-1. func1任务执行了
26 2-2. func1任务结束了
27 1-2. func1任务结束了
28 主程序
29 '''
基于greenlet实现协程[少用]

 

四   基于gevent模块实现协程调度

gevent提供的常用方法

方法描述
gevent.spawn(任务,任务参数) 创建greenlet协程对象
gevent.spawn(任务1,任务参数).link_value(回调处理函数,函数参数) 给协程对象注册结果回调处理函数
gevent.sleep(n) 异步的阻塞,没有真正阻塞。可以被协程识别。区别于time.sleep同步阻塞,不可被协程识别
gevent.joinall 基于libev事件循环实现多个协程阻塞等待执行结束
gevent.monkey.patch_all() 猴子补丁,给所有的会导致线程阻塞的方法或函数进行重写

Python的线程属于内核级别的,即由操作系统进行系统调度来控制的,如果单线程遇到IO或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行。而使用协程一旦遇到IO阻塞,就需要在代码中手动控制切换(而非操作系统,让渡),以此来提升效率。

我们怎么识别或检测到任务代码中的IO阻塞并自动切换调度到其他任务呢?

 1 import time
 2 import gevent
 3 
 4 def func():
 5     # 获取当前协程对象
 6     print("协程运行了!")
 7 
 8 if __name__ == '__main__':
 9     # 创建Greenlet协程对象
10     g1 = gevent.spawn(func)
11     # 阻塞3秒
12     gevent.sleep(3) # gevent的任务调度,需要自动检测到IO阻塞才会切换任务.因为这个IO阻塞的出现,所以执行到协程g1
13     # 相当于time.sleep(1),但是time.sleep无法被协程识别,
14     # 因为协程只能识别属于的异步的阻塞,而time.sleep属于一种同步的阻塞,所以无法切换调度到其他任务
15     # gevent 没有真正的阻塞,属于异步阻塞。time.sleep真正阻塞,属于同步阻塞
gevent实现协程调度

 

 1 import gevent
 2 
 3 
 4 def func1():
 5     print("1-1. func1任务执行了")
 6     gevent.sleep(2)
 7     print("1-2. func1任务结束了")
 8 
 9 def func2():
10     print("2-1. func1任务执行了")
11     gevent.sleep(1)
12     print("2-2. func1任务结束了")
13 
14 if __name__ == '__main__':
15     # 创建了2个greenlet协程
16     g1 = gevent.spawn(func1)
17     g2 = gevent.spawn(func2)
18     gevent.sleep(3)
19 
20 '''
21 1-1. func1任务执行了
22 2-1. func1任务执行了
23 2-2. func1任务结束了
24 1-2. func1任务结束了
25 '''
26 """
27 gevent的内部原理:
28 
29     gevent内部实现了libev事件循环(可以简单为死循环,),
30     我们调用gevent的spawn创建greenlet协程对象就是添加了一个协程对象到事件循环内部,类似如下:
31     while True:
32           greenlet.func1()
33           greenlet.func2()
34     
35     当程序代码运行时,也就是循环过程中遇到了gevent.sleep(3), 实际上,就是记录了当前调用gevent.sleep(3)的当前时间戳和阻塞等待时间戳而已。
36     假设在主程序中,先调用gevent.sleep(3),实际上就是在协程内部,使用time.time(),记录了当前时间戳(假设是x秒,),
37     还根据当前时间戳+阻塞的时间(此处假设3秒)得到阻塞等待时间戳,
38     那么当前线程中就会有一个列表(调度时间表):[(协程ID,x, x+3)],
39     接着就去切换到事件循环中下一个协程func1,如果协程有gevent.sleep(2),则进行再次使用time.time()记录当前时间戳,并记录x+2,
40     那么当前线程中的调度时间表变成:[(协程ID,x, x+3), (协程ID, x, x+2), ],
41     接着往下调度到另一个任务func2,执行func2的协程中如果再次遇到gevent.sleep(1),那么会再次使用time.time()记录当前时间戳,并记录x+1,
42     那么当前线程中的调度时间表变成:[(x+3, x, 协程ID为), (x+2,x, 协程func1), (x+1,x, 协程func2ID), ],
43     如果当前线程没有其他的协程了,那么调度时间表中使用min函数取出最小时间戳对应信息出来
44     min([(x+3, x, 协程ID为), (x+2,x, 协程func1), (x+1,x, 协程func2ID), ]),提取到(x+1, x, 协程func2)
45     判断时间是否到了,没到就阻塞等待,到了就直接执行对应的该时间戳的协程对应的代码func2
46     执行func2协程的过程中,如果没有再次遇到gevent.sleep的话,则协程直接执行结束,
47     主程序会再次从调度时间表使用min函数取出最小时间戳对应信息出来
48     min([(x+3, x, 协程ID为), (x+2,x, 协程func1)]),提取到(x+2, x, 协程func1)
49     再次等待1秒,时间到,执行对应的func1协程,协程执行如果没有再次遇到阻塞,
50     则再次从调度时间表使用min取出最小时间戳对应信息出来
51     min([(x+3, x, 协程ID为)]),提取到(x+3, x, 协程func1)
52     再次等待1秒,时间到,执行主程序了。
53 """
多任务遇到IO阻塞自动切换协程-gevent模块

 

 1 import time
 2 
 3 import gevent
 4 
 5 
 6 def func1():
 7     print("1-1. func1任务执行了")
 8     gevent.sleep(2)
 9     print("1-2. func1任务结束了")
10 
11 def func2():
12     print("2-1. func1任务执行了")
13     gevent.sleep(2)
14     print("2-2. func1任务结束了")
15 
16 if __name__ == '__main__':
17     task_list = [
18         gevent.spawn(func1),
19         gevent.spawn(func2)
20     ]
21     # g1.join()
22     # g2.join()
23     gevent.joinall(task_list)
24 '''
25 1-1. func1任务执行了
26 2-1. func1任务执行了
27 1-2. func1任务结束了
28 2-2. func1任务结束了
29 '''
多任务遇到IO阻塞自动切换协程-joinall方法

 

 1 import random
 2 import time
 3 import gevent
 4 
 5 
 6 def func(n):
 7     print(f"{n}-1. func{n}任务执行了")
 8     gevent.sleep(random.random())
 9     print(f"{n}-2. func{n}任务结束了")
10     return f"func{n}的结果"
11 
12 def callback(g):
13     """
14     协程任务的回调处理
15     :param g: 当前协程对象
16     :return:
17     """
18     print(g.value)
19 
20 if __name__ == '__main__':
21     task_list = []
22     for i in range(10):
23         # g greenlet协程对象
24         g = gevent.spawn(func, i)
25         # 给协程对象注册结果回调处理函数
26         g.link_value(callback)
27         # g.rawlink(callback)
28         task_list.append(g)
29 
30     gevent.joinall(task_list)
协程任务异步回调处理-gevent

 

猴子补丁【了解】

在很多的动态语言中,不改变源代码而对功能进行追加和变更的作用,都统称为猴子补丁。猴子补丁就是在模块运行的时候替换或重写模块中的某些方法或函数

我们需要使用由gevent提供的猴子补丁(monkey-patch)来对python常见的一些导致线程阻塞的函数或方法进行替换

 1 import time
 2 import gevent
 3 print(time.sleep)  # <built-in function sleep>
 4 # 在导包以后,在程序执行之前,给所有的会导致线程阻塞的方法或函数进行重写
 5 from gevent import monkey
 6 monkey.patch_all()
 7 print(time.sleep)  # <function sleep at 0x000001E561584B80>
 8 
 9 def func():
10     # 获取当前协程对象
11     print("协程func开始运行了!")
12     time.sleep(3)
13     print("协程func运行结束了!")
14 
15 if __name__ == '__main__':
16     # 创建Greenlet协程对象
17     g1 = gevent.spawn(func)
18     # time.sleep(1)  # 如果使用time则会导致当前阻塞会线程接管,而协程无法识别,也无法干扰
19     # python里面除了time.sleep以外,还有很多会导致线程阻塞的函数或方法,这些函数与方法都无法被协程识别或干扰
20     # 所以,我们需要使用由gevent提供的猴子补丁(monkey-patch)来对python常见的一些导致线程阻塞的函数或方法进行替换
21     time.sleep(3)
猴子补丁

 

协程池【了解】

协程和进程线程一样也有池(pool)的概念的,只是用于限制的并发数量,减轻系统对协程的创建与销毁的资源消耗(协程就是代码对象,所以能够减轻的程度是非常有效的)。

 1 import gevent
 2 from gevent import pool
 3 
 4 def func1():
 5     print("1-1, func1开始执行了")
 6     gevent.sleep(2)
 7     print("1-2, func1执行结束了")
 8 
 9 def func2():
10     print("2-1,func2开始执行了")
11     gevent.sleep(2)
12     print("2-2,func2执行结束了")
13 
14 if __name__ == '__main__':
15     # 协程池
16     p = pool.Pool()
17     p.apply_async(func1)
18     p.apply_async(func2)
19     p.join()
20 '''
21 1-1, func1开始执行了
22 2-1,func2开始执行了
23 1-2, func1执行结束了
24 2-2,func2执行结束了
25 '''
协程池

 

通过上面的代码,我们可以看到协程可以通过单线程内在多个上下文中进行来回切换执行,也可以看到协程在IO密集型操作中,可以利用在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调,那么就会大大节省资源并提升性能,从而实现异步编程也就是不等待任务结束就可以去执行其他代码。

当然,也要注意的是,协程在计算密集型操作中,如果利用协程来回频繁切换执行,实际上是没有任何意义,因为来回切换并保存代码执行状态反倒会导致程序降低性能。

因此对比操作系统控制线程的上下文切换,用户在单线程内控制协程的上下文切换会带来以下的优缺点和特点:

 

优点
  1. 无须像线程一样通过系统调度来进行上下文切换,较少了系统开销;

  2. 无须锁定及同步的操作,所以不需要加锁了。

  3. 主动切换代码的执行流程,简化编程的模型;

  4. 与进程、线程一样可以达到高并发性、高扩展性,而且比进程、线程要成本更低。

缺点
  1. 无法利用多核,因为协程的本质是单线程下工作,当然我们可以通过一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启多个协程来解决这个问题。

  2. 协程无法被操作系统感知,对于操作系统而言就是单个线程内部代码,因而一旦协程出现阻塞,将会阻塞整个线程,所以我们针对系统能感知的一些阻塞的代码使用猴子补丁进行规避。

特点
  1. 在只有一个单线程里就通过代码实现并发

  2. 修改共享数据不需加锁,协程不会出现同时对共享数据进行同时需改的情况。

  3. 用户程序里自己可以保存多个控制流的上下文栈(greelet与生成器帮我们实现协程的控制块)

  4. 协程遇到IO操作自动切换到其它协程(如何实现检测IO?yield、greenlet都无法实现,就用到了gevent模块或者asyncio模块来实现[它们怎么自动检车IO并实现上下文的切换,利用的IO多路复用的技术来实现的。gevent实际上本质就是IO多路复用技术里面select模型])

 

五 asyncio模块实现协程调度 重要

python3.4之前使用的都是gevent、eventlet、ternardo、twisted实现协程操作。

asyncio的编程模型就是一个事件循环。我们可以从asyncio模块中直接获取一个EventLoop事件循环的引用对象,然后把需要执行的协程任务注册到EventLoop事件循环中执行,就实现了异步协程了。

事件循环,可以把他当做是一个while循环,这个while循环在周期性的运行并执行一些任务,在特定条件下终止循环

# 伪代码
任务列表 = [ 任务1, 任务2, 任务3,... ]

while True:
可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行'和'已完成'的任务返回
for 就绪任务 in 已准备就绪的任务列表:
执行已就绪的任务

for 已完成的任务 in 已完成的任务列表:
在任务列表中移除 已完成的任务

如果 任务列表 中的任务都已完成,则终止循环

5.1 async & awiat

官方推荐使用async & awiat 关键字实现协程异步编程。await是一个只能在协程函数(使用 async 关键字标记的函数)中使用的关键字,用于遇到IO操作时挂起当前协程(任务),当前协程(任务)挂起过程中事件循环就可以自动切换去执行其他的协程(任务),当前协程IO处理挂起状态结束以后,会自动再次切换回来执行await之后的代码。

 1 import asyncio
 2 
 3 
 4 async def func():
 5     print("执行协程函数内部代码")
 6     # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。
 7     # 当前协程挂起时,事件循环可以去执行其他协程(任务)。
 8     response = await asyncio.sleep(2)
 9     print("IO请求结束,结果为:", response)  #None
10 
11 if __name__ == '__main__':
12     result = func() # 返回一个协程对象
13     asyncio.run(result) # 不能直接调用异步函数,需要使用asyncio模块来运行,否则警告
asyncya与wait

 

5.2 asyncio提供的常用方法

方法描述
await asyncio.sleep(delay, result) 异步阻塞指定之间,delay参数的值为异步阻塞时间,result的值为阻塞时间结束以后的返回结果
loop=asyncio.get_event_loop() 获得一个事件循环实例对象。
await asyncio.wait(fs) 并发地运行 fs 可迭代对象中的 可等待对象 并进入阻塞状态
asyncio.ensure_future(coro) 创建Task异步任务对象,coro为异步函数
loop.run_until_complete(future) 阻塞运行一个或多个异步任务future,future是异步函数返回的可等待对象
asyncio.create_task(coro) 创建Task异步任务对象,coro为异步函数
asyncio.run(main) 创建事件循环,运行一个协程,协程执行结束以后关闭事件循环。
asyncio.as_completed(fs) 从执行结束的异步任务队列中返回异步任务结果的迭代器
task.result() 获取异步任务的返回结果,task为Task异步任务对象
task.add_done_callback(fn) 设置异步任务的返回结果的异步回调函数,fn为函数名

 5.3 实例

 1 import asyncio
 2 
 3 # DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
 4 # 废弃警告:"@coroutine"装饰器,从python3.8版本中已经淘汰了,使用 "async def"替代
 5 @asyncio.coroutine
 6 def func1():
 7     print("1-1. func1任务执行了")
 8     yield from asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
 9     print("1-2. func1任务结束了")
10 
11 
12 
13 if __name__ == '__main__':
14     """方式1:"""
15     # # 1. 创建一个事件循环
16     # loop = asyncio.get_event_loop()
17     # # 2. 基于loop提供的run_until_complete就可以注册生成器对象到事件循环中,自动运行
18     # loop.run_until_complete(func1())
19 
20     """方式2:Python 3.7以后才能使用"""
21     # 本质上方式一是一样的,内部先 创建事件循环 然后执行 run_until_complete,一个简便的写法。
22     # asyncio.run 函数在 Python 3.7 中加入 asyncio 模块,
23     asyncio.run(func1())
24 
25 '''
26 DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
27   def func1():
28 1-1. func1任务执行了
29 1-2. func1任务结束了
30 '''
asyncio的基本使用

 

 1 import asyncio
 2 
 3 """
 4 def func():  # 同步函数
 5     pass
 6 """
 7 
 8 """定义一个异步函数(协程函数)"""
 9 async def func():
10     print("执行协程函数func开始执行了")
11     # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。
12     # 当前协程挂起时,事件循环可以去执行其他协程(任务)。
13     response = await asyncio.sleep(2)
14     print("IO请求结束,结果为:", response)
15 
16 if __name__ == '__main__':
17     asyncio.run(func())
18 
19 '''
20 执行协程函数func开始执行了
21 IO请求结束,结果为: None
22 '''
异步协程的基本语法

 

 1 import asyncio
 2 
 3 async def func1():
 4     print("协程函数func1开始执行了")
 5     await asyncio.sleep(2)
 6     return "func1的执行结果"
 7 
 8 async def func2():
 9     print("协程函数func2开始执行了")
10     # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。
11     # 当前协程挂起时,事件循环可以去执行其他协程(任务)。
12     response = await func1()
13     print("IO请求结束,结果为:", response)
14 
15 if __name__ == '__main__':
16     # 不能直接调用异步函数,需要使用asyncio模块来运行,否则警告
17     # print(func2())  # <coroutine object func2 at 0x00000172CC3B11C0>
18     asyncio.run(func2())
19 '''
20 协程函数func2开始执行了
21 协程函数func1开始执行了
22 IO请求结束,结果为: func1的执行结果
23 '''
异步协程的阻塞切换

 

 1 python3.7以前版本
 2 import asyncio
 3 
 4 async def func1():
 5     print("1-1. func1任务执行了")
 6     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
 7     print("1-2. func1任务结束了")
 8 
 9 
10 async def func2():
11     print("2-1. func2任务执行了")
12     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
13     print("2-2. func2任务结束了")
14 
15 if __name__ == '__main__':
16     # python3.7以前的asyncio是沒有run方法的,就要自己手动获取事件循环对象
17     # 注意:此处并非创建一个事件循环对象,这个事件循环对象在python中内部已经创建了
18     loop = asyncio.get_event_loop()
19 
20     # # 注册协程对象,返回task异步任务对象
21     # task1 = asyncio.ensure_future(func1())
22     # task2 = asyncio.ensure_future(func2())
23     #
24     # # 把task对象添加到协程的就绪(等待)列表
25     # task_list = asyncio.wait([task1,task2])
26     #
27     # # 把就需要列表中的所有task异步任务对象添加到事件循环中执行
28     # loop.run_until_complete(task_list)
29 
30     """简写操作"""
31     task_list = asyncio.wait([
32         asyncio.ensure_future(func1()),
33         asyncio.ensure_future(func2())
34     ])
35     loop.run_until_complete(task_list)
36 
37 '''
38 1-1. func1任务执行了
39 2-1. func2任务执行了
40 1-2. func1任务结束了
41 2-2. func2任务结束了
42 '''
43 
44 
45 
46 python3.7以后版本
47 
48 import asyncio
49 
50 async def func1():
51     print("1-1. func1任务执行了")
52     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
53     print("1-2. func1任务结束了")
54 
55 
56 async def func2():
57     print("2-1. func2任务执行了")
58     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
59     print("2-2. func2任务结束了")
60 
61 async def main():
62     print("main子协程开始执行")
63 
64     task_list = [
65         # 创建协程,将协程封装到一个Task对象中。
66         asyncio.create_task(func1(), name="f1"),
67         asyncio.create_task(func2(), name="f2"),
68     ]
69 
70     # 添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)
71     await asyncio.wait(task_list)
72     print("main子协程执行结束")
73 
74 if __name__ == '__main__':
75     asyncio.run(main())
多协程调度python3.7之前之后版本的两种写法

 

 1 python3.7之前
 2 
 3 import asyncio
 4 
 5 async def func():
 6     print("func任务执行了!")
 7     await asyncio.sleep(2)
 8     print("func任务结束了!")
 9     return 'func的执行结果'
10 
11 if __name__ == '__main__':
12     loop = asyncio.get_event_loop()
13     task = loop.create_task(func())
14     loop.run_until_complete(task)
15     ret = task.result()
16     print(f"函数的返回结果:{ret}")
17 
18 
19 
20 python3.7之后
21 
22 import asyncio
23 
24 
25 async def func():
26     print("func任务执行了!")
27     await asyncio.sleep(2)
28     print("func任务结束了!")
29     return 'func'
30 
31 if __name__ == '__main__':
32     task = asyncio.run(func())
33     print(f"函数的返回结果:{task}")
34 
35 '''
36 func任务执行了!
37 func任务结束了!
38 函数的返回结果:func
39 '''
获取异步任务函数的返回结果python3.7之前之后版本的两种写法

 

 1 import asyncio
 2 
 3 
 4 async def func():
 5     print("func任务执行了!")
 6     await asyncio.sleep(2)
 7     print("func任务结束了!")
 8     return 'func'
 9 
10 async def main():
11     print("main开始")
12     task = asyncio.create_task(func())
13     print("main结束")
14     # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
15     # 此处的await是等待相对应的协程全都执行完毕并获取结果
16     ret1 = await task
17     print(f"此处获取的ret1就是异步返回结果,ret1={ret1}")
18 
19 if __name__ == '__main__':
20     asyncio.run(main())
获取异步任务函数的返回结果python3.7之后版本的简写操作

 

 1 python3.7之前的版本
 2 import asyncio
 3 
 4 async def func1():
 5     print("1-1. func1任务执行了")
 6     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
 7     print("1-2. func1任务结束了")
 8     return "func1"
 9 
10 
11 async def func2():
12     print("2-1. func1任务执行了")
13     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
14     print("2-2. func1任务结束了")
15     return "func2"
16 
17 if __name__ == '__main__':
18     # 创建一个事件循环对象
19     loop = asyncio.get_event_loop()
20 
21     task_list = [
22         # 注册协程对象,返回task异步任务对象
23         loop.create_task(func1()),
24         loop.create_task(func2()),
25     ]
26     # 并发地运行 task_list 可迭代对象中的 可等待对象 并进入阻塞状态
27     wait_tasks = asyncio.wait(task_list)
28     # 列表中的一个/多个task异步任务对象添加到事件循环中执行
29     loop.run_until_complete(wait_tasks)
30 
31     for task in task_list:
32         # 获取异步任务的返回结果,task为Task异步任务对象
33         print(task.result())
34 '''
35 1-1. func1任务执行了
36 2-1. func1任务执行了
37 1-2. func1任务结束了
38 2-2. func1任务结束了
39 func1
40 func2
41 '''
42 
43 
44 python3.7之后的版本
45 import asyncio
46 
47 async def func1():
48     print("1-1. func1任务执行了")
49     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
50     print("1-2. func1任务结束了")
51     return "func1"
52 
53 
54 async def func2():
55     print("2-1. func1任务执行了")
56     await asyncio.sleep(2)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
57     print("2-2. func1任务结束了")
58     return "func2"
59 
60 async def main():
61     task_list = [
62         # 创建Task异步任务对象
63         asyncio.create_task(func1()),
64         asyncio.create_task(func2()),
65     ]
66     # 并发地运行 task_list 可迭代对象中的 可等待对象 并进入阻塞状态
67     await asyncio.wait(task_list)
68 
69     for task in task_list:
70         ret = await task
71         print(f"异步任务的返回结果:{ret}")
72 
73 
74 if __name__ == '__main__':
75     asyncio.run(main())
获取多个异步任务的返回结果python3.7之前之后版本的两种写法

 

 1 python3.7之前版本1
 2 import random
 3 import asyncio
 4 
 5 
 6 async def func(i):
 7     print(f"异步任务func{i}任务执行了")
 8     t = random.randint(1, 10)
 9     await asyncio.sleep(t)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
10     print(f"异步任务func{i}任务结束了")
11     return f"func{i}执行结果,耗时:{t}秒"  # 此处的返回值,最终在python底层会被分装一个可等待对象
12 
13 async def main():
14     task_list = []
15     # 注冊创建10个协程
16     for i in range(10):
17         task = asyncio.ensure_future(func(i))
18         task_list.append(task)
19 
20     for res in asyncio.as_completed(task_list):  # 从执行结束的任务队列中提取任务对象
21         print(res)  # 等待对象,实际上是asyncio.Future,叫可等待对象,可等待对象就可以使用await关键字提取结构
22         result = await res  # 因此await后面必须是asyncio封装的可等待对象
23         print(result)
24 
25 if __name__ == '__main__':
26     # 创建一个事件循环对象
27     loop = asyncio.get_event_loop()
28     # 列表中的一个/多个task异步任务对象添加到事件循环中执行
29     loop.run_until_complete(main())
30  
31 
32 python3.7之前版本2
33 import random
34 import asyncio
35 
36 
37 async def func(i):
38     print(f"异步任务func{i}任务执行了")
39     t = random.randint(1, 10)
40     await asyncio.sleep(t)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
41     print(f"异步任务func{i}任务结束了")
42     return f"func{i}执行结果,耗时:{t}秒"  # 此处的返回值,最终在python底层会被分装一个可等待对象
43 
44 
45 def callback(res):
46     print(res.result())
47 
48 async def main():
49     task_list = []
50     # 注冊创建10个协程
51     for i in range(10):
52         task = asyncio.ensure_future(func(i))
53         task.add_done_callback(callback)
54         task_list.append(task)
55 
56     await asyncio.wait(task_list)
57 
58 if __name__ == '__main__':
59     loop = asyncio.get_event_loop()
60     loop.run_until_complete(main())
61 
62 
63 
64 python3.7之后版本
65 import random
66 import asyncio
67 
68 
69 async def func(i):
70     print(f"异步任务func{i}任务执行了")
71     t = random.randint(1, 10)
72     await asyncio.sleep(t)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
73     print(f"异步任务func{i}任务结束了")
74     return f"func{i}执行结果,耗时:{t}秒"  # 此处的返回值,最终在python底层会被分装一个可等待对象
75 
76 
77 def callback(res):
78     print(res.result())
79 
80 async def main():
81     task_list = []
82     # 注冊创建10个协程
83     for i in range(10):
84         task = asyncio.create_task(func(i))
85         task.add_done_callback(callback)
86         task_list.append(task)
87 
88     await asyncio.wait(task_list)
89 
90 if __name__ == '__main__':
91     asyncio.run(main())
多协程任务的结果进行异步回调python3.7之前之后版本

 

uvloop是一个第三方模块,专门用于替代asyncio内置的事件循环loop的。替代了以后,可以让asyncio得到性能的提高,理论上使用uvloop以后的asyncio比原来没有替代前,提升2倍的执行效率,性能可以追上go的协程性能。

 1 import random
 2 import asyncio
 3 
 4 import uvloop
 5 asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
 6 
 7 async def func():
 8     print(f"异步任务func任务执行了")
 9     await asyncio.sleep(3)  # 遇到IO耗时操作,自动化切换到tasks中的其他任务
10     print(f"异步任务func任务结束了")
11     return f"func执行结果"  # 此处的返回值,最终在python底层会被分装一个可等待对象
12 
13 if __name__ == '__main__':
14     ret = asyncio.run(func())
15     print(ret)
高性能的事件循环uvloop

 

Future对象

可等待对象(awaitable),就是能在await表达式中使用的对象,可以是coroutine 或是具有__await__()方法的对象可等待对象的__await__方法的返回值必须是一个迭代器对象

注意:支持 await xxx语法的xxx对象都是可等待对象,所以协程对象、Task对象、Future对象都是可等待对象

import asyncio

class B(object):
    """迭代器"""
    def __iter__(self):
        return self

    def __next__(self):
        raise StopIteration('end')


class A(object):
    """可等待对象的类"""
    def __await__(self):
        return B()  # B()就是一个迭代器


async def main():
    s = await A()  # 可等待对象
    print(s)


if __name__ == '__main__':
    task = main()
    loop = asyncio.run(task)
# end
可等待对象

 

我们点击查看结果处理源码的话,可以发现,是一个叫Future类提供的,那么这个Future类是什么呢?

实际上我们编写异步协程中所有的方法的结果处理操作,实际上都是基于Future对象提供的操作来完成的。Future是一个相对更偏向底层的可等待对象(awaitable object),它提供了异步编程中的最终结果的处理操作,所以我们一般把Future也叫异步回调结果对象,虽然平时使用的是Task对象,但对于结果的处理本质是基于Future对象来实现的,Task是Futrue的子类。

实例一
import asyncio


async def func():
    print("func任务执行了!")
    await asyncio.sleep(2)
    print("func任务结束了!")
    return 'func的执行结果'


async def main():
    # loop = asyncio.get_event_loop()
    # task = loop.create_task(func())
    # print(func())  # coroutine 协程对象
    task = asyncio.create_task(func())  # 这句代码就是上面2句代码的简写
    # print(task)  # 可以打印得到Task对象
    ret1 = await task
    # ret1 = task.result() # 此处直接获取结果是不行的,因为当前方法执行的时候,当前异步任务并没有被加入就绪队列中
    print(f"ret1={ret1}")

if __name__ == '__main__':
    asyncio.run(main())



实例二
import asyncio
import inspect
from asyncio.futures import Future
from asyncio.tasks import  Task

async def func():
    print("func任务执行了!")
    await asyncio.sleep(2)
    print("func任务结束了!")
    return 'func的执行结果'

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    task = loop.create_task(func())
    loop.run_until_complete(task)
    ret = task.result()  # 此处获取结果,因此await task 的作用就是等价于这句话,只是await使用过程,语法要求必须把await写在协程函数中
    print(f"函数的返回结果:{ret}")

    # print(inspect.iscoroutine(func()))  # True func()的返回值是一个协程对象
    print(inspect.isawaitable(task))      # True task是一个可等待对象
    print(isinstance(task, Task))         # True 证明了task对象是Task类的实例
    print(issubclass(Task, Future))       # True 证明了Task类是Future的子类
    print(isinstance(task, Future))       # True

'''
我们点击result查看源码,可以发现,实际上result方法根本不是Task类提供的,而是一个叫Future类提供的,
那么这个Future类是什么呢?
实际上我们编写异步协程中所有的方法的结果处理操作,实际上都是基于Future对象提供的操作来完成的。
Future是一个相对更偏向底层的可等待对象(awaitable object),它提供了异步编程中的最终结果的处理操作,
所以我们一般把Future也叫异步回调结果对象,虽然平时使用的是Task对象,但对于结果的处理本质是基于Future对象来实现的,
Task是Futrue的子类。
'''
异步协程中所有方法的结果处理操作-引出future对象

 

import asyncio


async def func(fut):
    print("func1任务执行了!")
    await asyncio.sleep(2)
    print("func1任务结束了!")
    fut.set_result("func1执行结果")
    # return "func1执行结果"  # 在task对象的内部,使用Future.set_result进行了结果的设置
    # 没有返回值了!!!

async def main():
    # 获取当前事件循环
    loop = asyncio.get_running_loop()
    # 创建一个任务(Future对象),没绑定任何行为,则这个任务永远不知道什么时候结束。
    fut = loop.create_future()
    # 创建一个任务(Task对象),绑定了func协程任务函数,函数内部在2s之后通过fut.set_result设置返回值。
    # 即手动设置future任务的最终结果,那么fut就可以结束了。
    await loop.create_task(func(fut))
    # 等待 Future对象获取 最终结果,否则一直等下去
    data = await fut
    print(f"{data=}")


if __name__ == '__main__':
    asyncio.run(main())
    
'''
func1任务执行了!
func1任务结束了!
data='func1执行结果'
'''
'''
Future对象本身与协程任务函数之间不进行绑定,所以想要让事件循环获取Future的结果,则需要手动设置。
而Task对象继承了Future对象,其实就对Future对象进行扩展,他可以实现在对应绑定的函数执行完成之后,
自动执行`set_result`,从而实现自动结束,自动返回结果给await Future对象语句。
'''
Future对象的工作流程

 

在项目以协程式的异步编程开发时,如果要使用一个第三方模块,那么这个模块必须要实现异步才能与协程交替执行,否则要么报错,要么阻塞执行。所以为了提高程序的性能需要把不支持协程式异步编程的第三方模块转换成异步。

asyncio模块中的时间循环对象提供了run_in_exeutor把本身不支持异步的对象转换成future异步对象,基于这个方法可以实现同步任务转换成异步任务的效果

import asyncio
import os.path

# requests默认是不支持协程异步的,是一个同步网络请求模块
import requests


async def get_img(i, url):
    # 发送网络请求,下载图片
    print(f"开始下载{i+1}:", url)
    # 同步代码,发起网络请求
    # response = requests.get(url)
    # 同步转异步,让程序遇到网络下载图片的IO请求,自动化切换到其他任务
    loop = asyncio.get_running_loop()
    # run_in_executor(None, 同步阻塞函数或方法名, 函数的参数1,,函数的参数2,函数的参数3....)
    response = await loop.run_in_executor(None, requests.get, url)
    print(f'第{i+1}张图片下载完成')
    # 图片保存到本地文件
    filename = os.path.basename(url)
    with open(f"images/{filename}", mode='wb') as f:
        f.write(response.content)

if __name__ == '__main__':
    url_list = [
        'https://pic.netbian.com/uploads/allimg/220512/011323-16522892039531.jpg',
        'https://pic.netbian.com/uploads/allimg/210831/102129-163037648996ad.jpg',
        'https://pic.netbian.com/uploads/allimg/210827/235918-1630079958cd73.jpg'
    ]
    tasks = [get_img(i, url) for i, url in enumerate(url_list)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))


代码优化
import asyncio
import os.path

# requests默认是不支持协程异步的,是一个同步网络请求模块
import requests


async def get_img(i, url):
    # 发送网络请求,下载图片
    print(f"开始下载{i+1}:", url)
    # 同步代码,发起网络请求
    # response = requests.get(url)
    # 同步转异步,让程序遇到网络下载图片的IO请求,自动化切换到其他任务
    loop = asyncio.get_running_loop()
    # run_in_executor(None, 同步阻塞函数或方法名, 函数的参数1,,函数的参数2,函数的参数3....)
    response = await loop.run_in_executor(None, requests.get, url)
    await loop.run_in_executor(None, save_image, url, response)
    print(f'第{i+1}张图片下载完成')


def save_image(url, response):
    # 图片保存到本地文件
    filename = os.path.basename(url)
    with open(f"images/{filename}", mode='wb') as f:
        f.write(response.content)

if __name__ == '__main__':
    url_list = [
        'https://pic.netbian.com/uploads/allimg/220512/011323-16522892039531.jpg',
        'https://pic.netbian.com/uploads/allimg/210831/102129-163037648996ad.jpg',
        'https://pic.netbian.com/uploads/allimg/210827/235918-1630079958cd73.jpg'
    ]
    tasks = [get_img(i, url) for i, url in enumerate(url_list)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
同步代码异步化

 

import time
import asyncio
from asyncio.futures import Future as Future1
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
from concurrent.futures import Future as Future2

def func1():
    print("func1任务执行了!")
    time.sleep(2)
    print("func1任务结束了!")
    return "func1执行结果"


def func2():
    print("func2任务执行了!")
    time.sleep(2)
    print("func2任务结束了!")
    return "func2执行结果"


async def main():
    loop = asyncio.get_running_loop()
    """
    loop.run_in_executor的第1个参数实际上是池(Pool),如果传递的参数值是None,则默认创建线程池,然后把func1放到线程池中异步执行
    也就是说loop.run_in_executor的内部底层代码的执行流程:
    第1步:内部会先调用ThreadPoolExecutor的submit方法去线程池中申请一个线程去执行func1函数,并返回一个concurrent.futures.Future对象
    第2步:调用asyncio.wrap_future将concurrent.futures.Future对象包装为asyncio.Future对象。
    因为concurrent.futures.Future对象没有实现__await__方法,所以不支持await语法,所以需要包装为 asycio.Future对象 才能使用。
    """

    # fut1 = loop.run_in_executor(None, func1)
    # fut2 = loop.run_in_executor(None, func2)
    # print(type(fut1) is Future1)  # True
    # print(type(fut1) is Future2)  # False
    #
    # result1 = await fut1
    # result2 = await fut2
    #
    # print(f"{result1=}")
    # print(f"{result2=}")

    # 2. 把同步函数放到线程池中,申请一个线程异步执行函数
    with ThreadPoolExecutor() as pool:  # 创建一个线程池
        fut = loop.run_in_executor(pool, func1)  # 把func1放到线程池申请一个线程异步执行,并把结果包装成asyncio的Future对象
        print(type(fut))
        result = await fut
        print(f"线程:{result=}")

    # 3. 把同步函数放到进程池中,申请一个进程异步执行函数
    with ProcessPoolExecutor() as pool:
        fut = loop.run_in_executor(pool, func1)
        print(fut)
        result = await fut
        print(f"进程:{result=}")


if __name__ == '__main__':
    asyncio.run(main())
同步转异步的原理

 

默认的可迭代对象是无法被使用在async for语句中的,原因是默认的可迭代对象并没有实现__aiter__方法。

异步可迭代对象:可以使用async for进行遍历,则遍历过程中,执行的是 __aiter__方法,__aiter__方法的返回值必须是异步迭代器。

import asyncio
import random
import time


class Time(object):
    """ 异步迭代器 """
    def __aiter__(self):
        return self

    async def __anext__(self):
        val = await asyncio.sleep(random.random(), time.time())
        if not val:
            raise StopAsyncIteration
        return val


# list本身只是一个可迭代对象的类
# 基于list创建了子类List,只实现了__aiter__方法,所以List类创建出来的对象就是一个异步可迭代对象。
class List(list):
    """异步可迭代对象"""
    def __aiter__(self):
        return Time()

async def main():
    # 循环异步可迭代对象,可以使用async for进行遍历,则遍历过程中,执行的是 __aiter__方法,__aiter__方法的返回值必须是异步迭代器
    # 注意:如果把异步可迭代对象,使用for进行遍历,则遍历过程中,执行的是__iter__方法,__iter__方法的返回值必须是迭代器
    async for item in List([1, 2, 3, 3]):
        print(item)

if __name__ == '__main__':
    asyncio.run(main())

'''
1653740947.9042292
1653740948.8492758
1653740949.534409
...
'''
异步可迭代对象

 

异步迭代器:实现了__aiter__()__anext__() 方法的异步可迭代对象。__anext__ 必须返回一个可等待对象(awaitable)。同理,默认的迭代器也是无法被使用在async for语句中的。

import asyncio
import random
import time


class Time(object):
    """ 异步迭代器 """
    def __aiter__(self):
        return self

    async def __anext__(self):
        val = await asyncio.sleep(1, time.time())
        if not val:
            raise StopAsyncIteration
        return val


async def main():
    # 同步的迭代器与异步迭代器在提取数据的时候,都是基于同步获取结果的。
    async for item in Time():
        print(item)

if __name__ == '__main__':
    asyncio.run(main())
异步迭代器

 

 

异步上下文管理器

只要对象定义了 __aenter__()__aexit__() 方法就是一个异步上下文管理器对象, 可以 async with 语句中的环境进行上下文控制。

import socket
import asyncio


class Sniffer(object):
    """网络嗅探器"""
    def __init__(self, url, port, timeout=3):
        self.url = url
        self.port = port
        self.timeout = timeout
        self.socket = socket.socket()
        self.socket.settimeout(timeout)

    async def connect(self):
        """嗅探连接远程服务器,查看指定端口是否开启了"""
        loop = asyncio.get_running_loop()
        try:
            await loop.run_in_executor(None, self.socket.connect, (self.url, self.port))
            return True   # 表示当前端口是开放的
        except:
            return False  # 表示当前端口是没有开放

    async def __aenter__(self):
        print(f"开始连接端口:{self.port}")
        self.result = await self.connect()
        return self.result

    async def __aexit__(self, exc_type, exc, tb):
        """异步关闭远程socket连接"""
        if self.socket:
            self.socket.close()
            print(f"结束连接端口:{self.port}")


async def main():
    async with Sniffer("127.0.0.1", 22) as f:
        '''执行__aenter__,with语句结束后自动执行__aexit__'''
        print(f)

if __name__ == '__main__':
    asyncio.run(main())

'''
开始连接端口:22
True
结束连接端口:22
'''
异步上下文管理器-网络嗅探器

 

异步http网络模块-aiohttp

自从python出了asyncio以后,python的异步编程就不再局限于进程和线程以及之前采用各种方式途径实现的协程。python开发中基于asyncio发展出了非常多适用于实现异步编程的各种模块。

对于我们前面使用的requests这个http网络请求模块,实际上在异步编程里面,因为requests的网络请求会被线程识别阻塞,所以针对异步编程下,就有开发者开发了异步编程里面的异步网络请求模块,其中比较常用的有httpx与aiohttp,httpx的性能比requests快,但是aiohttp要慢,因此在异步编程中,我们经常使用的是aiohttp。

aiohttp基于requests模块的异步实现,但是比requests要功能更多,因此aiohttp的部分功能使用时与requests的使用非常类似的,但是aiohttp必须配合asyncio模块来进行使用。aiohttp不仅可以发送网络请求,还可以实现异步http web服务器。

import asyncio
import aiohttp
"""针对Event loop is closed报错的解决方案有3种方案"""
# 1. linux或mac X OS系统下 改成uvloop
# import uvloop
# asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

# 2. windows系统
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

async def func():
    async with aiohttp.ClientSession() as session:
        async with session.get("https://pic.netbian.com/uploads/allimg/220512/011323-16522892039531.jpg") as response:
            with open("1.png", "wb") as f:
                f.write(await response.read())

if __name__ == '__main__':
    asyncio.run(func())
    # 3. 不要使用run方法,改成python3.7以前的写法
    # loop = asyncio.get_event_loop()
    # loop.run_until_complete(func())
aiohttp基本使用

 

 

 

posted on 2022-05-26 22:18  大明花花  阅读(338)  评论(0编辑  收藏  举报