From :廖雪峰 异步IO :https://www.liaoxuefeng.com/wiki/1016959663602400/1017959540289152
Python Async/Await入门指南 :https://zhuanlan.zhihu.com/p/27258289
Python 生成器 和 yield 关键字:https://blog.csdn.net/freeking101/article/details/51126293
协程与任务 官网文档:https://docs.python.org/zh-cn/3/library/asyncio-task.html
Python中异步协程的使用方法介绍:https://blog.csdn.net/freeking101/article/details/88119858
python 协程详解及I/O多路复用,I/O异步:https://blog.csdn.net/u014028063/article/details/81408395
Python协程深入理解:https://www.cnblogs.com/zhaof/p/7631851.html
asyncio 进阶:Python黑魔法 --- 异步IO( asyncio) 协程:https://www.cnblogs.com/dhcn/p/9033628.html
谈谈Python协程技术的演进:https://www.freebuf.com/company-information/153421.html
最后推荐一下《流畅的Python》,这本书中 第16章 协程的部分介绍的非常详细
《流畅的Python》pdf 下载地址:https://download.csdn.net/download/freeking101/10993120
gevent 是 python 的一个并发框架,以微线程 greenlet 为核心,使用了 epoll 事件监听机制以及诸多其他优化而变得高效。
aiohttp 使用代理 ip 访问 https 网站报错的问题:https://blog.csdn.net/qq_43210211/article/details/108379917
Python:使用 Future、asyncio 处理并发
:https://blog.csdn.net/sinat_38682860/article/details/105419842
异步 IO
在 IO 编程( 廖雪峰 Python IO 编程 :https://www.liaoxuefeng.com/wiki/1016959663602400/1017606916795776) 一节中,我们已经知道,CPU的速度远远快于磁盘、网络等IO。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到IO操作,如读写文件、发送网络数据时,就需要等待IO操作完成,才能继续进行下一步操作。这种情况称为同步IO。
在IO操作的过程中,当前线程被挂起,而其他需要CPU执行的代码就无法被当前线程执行了。
因为一个 IO 操作就阻塞了当前线程,导致其他代码无法执行,所以我们必须使用多线程或者多进程来并发执行代码,为多个用户服务。每个用户都会分配一个线程,如果遇到IO导致线程被挂起,其他用户的线程不受影响。
多线程和多进程的模型虽然解决了并发问题,但是系统不能无上限地增加线程。由于系统切换线程的开销也很大,所以,一旦线程数量过多,CPU的时间就花在线程切换上了,真正运行代码的时间就少了,结果导致性能严重下降。
由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配,多线程和多进程只是解决这一问题的一种方法。
另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时,它只发出IO指令,并不等待IO结果,然后就去执行其他代码了。一段时间后,当IO返回结果时,再通知CPU进行处理。
消息模型 其实早在应用在桌面应用程序中了。一个 GUI 程序的主线程就负责不停地读取消息并处理消息。所有的键盘、鼠标等消息都被发送到GUI程序的消息队列中,然后由GUI程序的主线程处理。
由于GUI 线程处理键盘、鼠标等消息的速度非常快,所以用户感觉不到延迟。某些时候,GUI线程在一个消息处理的过程中遇到问题导致一次消息处理时间过长,此时,用户会感觉到整个GUI程序停止响应了,敲键盘、点鼠标都没有反应。这种情况说明在消息模型中,处理一个消息必须非常迅速,否则,主线程将无法及时处理消息队列中的其他消息,导致程序看上去停止响应。
消息模型 是 如何解决 同步IO 必须等待IO操作这一问题的呢 ?
在消息处理过程中,当遇到 IO 操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。
在 “发出IO请求” 到收到 “IO完成” 的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。
协程 (Coroutines)
在学习异步IO模型前,我们先来了解协程,协程 又称 微线程,纤程,英文名 Coroutine。
子程序( 又叫 函数 ) 和 协程
- 子程序 在 所有语言中都是层级调用。比如: A 调用 B,B 在执行过程中又调用了 C,C 执行完毕返回,B 执行完毕返回,最后是 A 执行完毕。所以 子程序 即 函数 的调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。
- 协程的调用 和 子程序 不同。协程 看上去也是 子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个 子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如:子程序 A 和 B :
def A():
print('1')
print('2')
print('3')
def B():
print('x')
print('y')
print('z')
假设由协程执行,在执行 A 的过程中,可以随时中断,去执行 B,B 也可能在执行过程中中断再去执行 A,结果可能是:
1
2
x
y
3
z
但是在 A 中是没有调用 B 的,所以 协程的调用 比 函数调用 理解起来要难一些。
看起来 A、B 的执行有点像多线程,但 协程 的特点在于是一个线程执行。
协程 和 多线程比,协程有何优势?
- 1. 最大的优势就是协程极高的执行效率。因为 子程序 切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
- 2. 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?
最简单的方法是 多进程 + 协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
Python 对 协程 的支持 是通过 generator (生成器)实现的
在 generator 中,我们不但可以通过 for 循环来迭代,还可以不断调用 next() 函数获取由 yield 语句返回的下一个值。
但是 Python 的 yield 不但可以返回一个值,它还可以接收调用者发出的参数。
来看例子:
传统的 生产者-消费者 模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过 yield 跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Author :
# @File : text.py
# @Software : PyCharm
# @description : XXX
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
执行结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
注意到 consumer函数 是一个 generator,把一个 consumer 传入 produce 后:
- 首先调用 c.send(None) 启动生成器;
- 然后,一旦生产了东西,通过 c.send(n) 切换到 consumer 执行;
-
consumer 通过 yield拿到消息,处理,又通过yield把结果传回;
-
produce 拿到 consumer 处理的结果,继续生产下一条消息;
-
produce 决定不生产了,通过 c.close() 关闭 consumer,整个过程结束。
整个流程无锁,由一个线程执行,produce
和 consumer
协作完成任务,所以称为 “协程”,而非线程的抢占式多任务。
最后套用 Donald Knuth 的一句话总结协程的特点:“子程序就是协程的一种特例。”
参考源码:https://github.com/michaelliao/learn-python3/blob/master/samples/async/coroutine.py
在 Python 中,异步函数 通常 被称作 协程
创建一个协程仅仅只需使用 async 关键字,或者使用 @asyncio.coroutine 装饰器。下面的任一代码,都可以作为协程工作,形式上也是等同的:
import asyncio
# 方式 1
async def ping_server(ip):
pass
# 方式 2
@asyncio.coroutine
def load_file(path):
pass
上面这两个 特殊的函数,在调用时会返回协程对象。熟悉 JavaScript 中 Promise 的同学,可以把这个返回对象当作跟 Promise 差不多。调用他们中的任意一个,实际上并未立即运行,而是返回一个协程对象,然后将其传递到 Eventloop 中,之后再执行。
- 如何判断一个 函数是不是协程 ? asyncio 提供了 asyncio.iscoroutinefunction(func) 方法。
- 如何判断一个 函数返回的是不是协程对象 ? 可以使用 asyncio.iscoroutine(obj) 。
用 asyncio 提供的 @asyncio.coroutine 可以把一个 generator 标记为 coroutine 类型,然后在 coroutine 内部用 yield from 调用另一个 coroutine 实现异步操作。
Python 3.5 开始引入了新的语法 async 和 await
为了简化并更好地标识异步 IO,从 Python 3.5 开始引入了新的语法 async 和 await,可以让 coroutine 的代码更简洁易读。
async / await 是 python3.5 的新语法,需使用 Python3.5 版本 或 以上才能正确运行。
注意:async 和 await 是针对 coroutine 的新语法,要使用新的语法,只需要做两步简单的替换:
- 把 @asyncio.coroutine 替换为 async
- 把 yield from 替换为 await
Python 3.5 以前 版本原来老的语法使用 协程
import asyncio
@asyncio.coroutine
def hello():
print("Hello world!")
r = yield from asyncio.sleep(1)
print("Hello again!")
Python 3.5 以后 用新语法重新编写如下:
import asyncio
async def hello():
print("Hello world!")
r = await asyncio.sleep(1)
print("Hello again!")
在过去几年内,异步编程由于某些好的原因得到了充分的重视。虽然它比线性编程难一点,但是效率相对来说也是更高。
比如,利用 Python 的 异步协程 (async coroutine) ,在提交 HTTP 请求后,就没必要等待请求完成再进一步操作,而是可以一边等着请求完成,一边做着其他工作。这可能在逻辑上需要多些思考来保证程序正确运行,但是好处是可以利用更少的资源做更多的事。
即便逻辑上需要多些思考,但实际上在 Python 语言中,异步编程的语法和执行并不难。跟 Javascript 不一样,现在 Python 的异步协程已经执行得相当好了。
对于服务端编程,异步性似乎是 Node.js 流行的一大原因。我们写的很多代码,特别是那些诸如网站之类的高 I/O 应用,都依赖于外部资源。这可以是任何资源,包括从远程数据库调用到 POST 一个 REST 请求。一旦你请求这些资源的任一一个,你的代码在等待资源响应时便无事可做 (译者注:如果没有异步编程的话)。
有了异步编程,在等待这些资源响应的过程中,你的代码便可以去处理其他的任务。
Python async / await 手册
Python 部落:Python async/await 手册:https://python.freelycode.com/contribution/detail/57
知乎:从 0 到 1,Python 异步编程的演进之路( 通过爬虫演示进化之路 ):https://zhuanlan.zhihu.com/p/25228075
async / await 的使用
async 用来声明一个函数是协程,然后使用 await 调用这个协程, await 必须在函数内部,这个函数通常也被声明为另一个协程。await 的目的是等待协程控制流的返回。yield 的目的 是 暂停并挂起函数的操作。
正常的函数在执行时是不会中断的,所以你要写一个能够中断的函数,就需要添加 async 关键。
- async 用来声明一个函数为异步函数,异步函数的特点是能在函数执行过程中挂起,去执行其他异步函数,等到挂起条件(假设挂起条件是sleep(5))消失后,也就是5秒到了再回来执行。
- await 可以将耗时等待的操作挂起,让出控制权( await 语法来挂起自身的协程 )。比如:异步程序执行到某一步时需要等待的时间很长,就将此挂起,去执行其他的异步程序。await 后面只能跟 异步程序 或 有 __await__ 属性 的 对象,因为异步程序与一般程序不同。
假设有两个异步函数 async a,async b,a 中的某一步有 await,当程序碰到关键字 await b() 后,异步程序挂起后去执行另一个异步b程序,就是从函数内部跳出去执行其他函数,当挂起条件消失后,不管b是否执行完,要马上从b程序中跳出来,回到原程序执行原来的操作。如果 await 后面跟的 b 函数不是异步函数,那么操作就只能等 b 执行完再返回,无法在 b 执行的过程中返回。如果要在 b 执行完才返回,也就不需要用 await 关键字了,直接调用 b 函数就行。所以这就需要 await 后面跟的是 异步函数了。在一个异步函数中,可以不止一次挂起,也就是可以用多个 await 。
看下 Python 中常见的几种函数形式:
# 1. 普通函数
def function():
return 1
# 2. 生成器函数
def generator():
yield 1
# 在3.5过后,我们可以使用async修饰将普通函数和生成器函数包装成异步函数和异步生成器。
# 3. 异步函数(协程)
async def async_function():
return 1
# 4. 异步生成器
async def async_generator():
yield 1
通过类型判断可以验证函数的类型
import types
# 1. 普通函数
def function():
return 1
# 2. 生成器函数
def generator():
yield 1
# 在3.5过后,我们可以使用async修饰将普通函数和生成器函数包装成异步函数和异步生成器。
# 3. 异步函数(协程)
async def async_function():
return 1
# 4. 异步生成器
async def async_generator():
yield 1
print(type(function) is types.FunctionType)
print(type(generator()) is types.GeneratorType)
print(type(async_function()) is types.CoroutineType)
print(type(async_generator()) is types.AsyncGeneratorType)
直接调用异步函数不会返回结果,而是返回一个coroutine对象:
print(async_function())
# <coroutine object async_function at 0x102ff67d8>
协程 需要通过其他方式来驱动,因此可以使用这个协程对象的 send 方法给协程发送一个值:
print(async_function().send(None))
不幸的是,如果通过上面的调用会抛出一个异常:StopIteration: 1
因为 生成器 / 协程 在正常返回退出时会抛出一个 StopIteration 异常,而原来的返回值会存放在 StopIteration 对象的 value 属性中,通过以下捕获可以获取协程真正的返回值:
try:
async_function().send(None)
except StopIteration as e:
print(e.value)
# 1
通过上面的方式来新建一个 run 函数来驱动协程函数,在协程函数中,可以通过 await 语法来挂起自身的协程,并等待另一个 协程 完成直到返回结果:
def run(coroutine):
try:
coroutine.send(None)
except StopIteration as e:
return 'run() : return {0}'.format(e.value)
async def async_function():
return 1
async def await_coroutine():
result = await async_function()
print('await_coroutine() : print {0} '.format(result))
ret_val = run(await_coroutine())
print(ret_val)
要注意的是,await 语法只能出现在通过 async 修饰的函数中,否则会报 SyntaxError 错误。
而且 await 后面的对象需要是一个 Awaitable,或者实现了相关的协议。
查看 Awaitable 抽象类的代码,表明了只要一个类实现了__await__方法,那么通过它构造出来的实例就是一个 Awaitable:
class Awaitable(metaclass=ABCMeta):
__slots__ = ()
@abstractmethod
def __await__(self):
yield
@classmethod
def __subclasshook__(cls, C):
if cls is Awaitable:
return _check_methods(C, "__await__")
return NotImplemented
而且可以看到,Coroutine类 也继承了 Awaitable,而且实现了 send,throw 和 close 方法。所以 await 一个调用异步函数返回的协程对象是合法的。
class Coroutine(Awaitable):
__slots__ = ()
@abstractmethod
def send(self, value):
...
@abstractmethod
def throw(self, typ, val=None, tb=None):
...
def close(self):
...
@classmethod
def __subclasshook__(cls, C):
if cls is Coroutine:
return _check_methods(C, '__await__', 'send', 'throw', 'close')
return NotImplemented
接下来是异步生成器,来看一个例子:
假如我要到一家超市去购买土豆,而超市货架上的土豆数量是有限的:
class Potato:
@classmethod
def make(cls, num, *args, **kws):
potatos = []
for i in range(num):
potatos.append(cls.__new__(cls, *args, **kws))
return potatos
all_potatos = Potato.make(5)
现在我想要买50个土豆,每次从货架上拿走一个土豆放到篮子:
def take_potatos(num):
count = 0
while True:
if len(all_potatos) == 0:
sleep(.1)
else:
potato = all_potatos.pop()
yield potato
count += 1
if count == num:
break
def buy_potatos():
bucket = []
for p in take_potatos(50):
bucket.append(p)
对应到代码中,就是迭代一个生成器的模型,显然,当货架上的土豆不够的时候,这时只能够死等,而且在上面例子中等多长时间都不会有结果(因为一切都是同步的),也许可以用多进程和多线程解决,而在现实生活中,更应该像是这样的:
import asyncio
import random
class Potato:
@classmethod
def make(cls, num, *args, **kws):
potatos = []
for i in range(num):
potatos.append(cls.__new__(cls, *args, **kws))
return potatos
all_potatos = Potato.make(5)
async def take_potatos(num):
count = 0
while True:
if len(all_potatos) == 0:
await ask_for_potato()
potato = all_potatos.pop()
yield potato
count += 1
if count =&