一. 定义协程
协程是轻量级线程,拥有自己的寄存机上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,再切回来时,恢复先前保存的寄存器上下文和栈。
协程的应用场景:I/O密集型任务,和多线程类似,但协程调用时在一个线程内进行的,是单线程,切换的开销小,因此,效率上略高于多线程。
python3.4加入了协程,以生成器对象为基础,python3.5加了async/await,使用协程更加方便。
python中使用协程最方便的库是asyncio,引入该库才能使用async和await关键字
- async: 定义一个协程;async定义的方法无法直接执行,必须注册到时间循环中才能执行。
- await: 用于临时挂起一个函数或方法的执行。
根据官方文档,await后面的对象必须是如下类型之一:
- 一个原生的coroutine对象;
- 一个由types.coroutine()修饰的生成器,这个生成器可以返回coroutine对象;
- 一个包含await方法的对象返回的一个迭代器。
案例1:
import asyncio
import time
async def task():
print(f"{time.strftime('%H:%M:%S')} task 开始 ")
time.sleep(2)
print(f"{time.strftime('%H:%M:%S')} task 结束")
coroutine = task()
print(f"{time.strftime('%H:%M:%S')} 产生协程对象 {coroutine},函数并未被调用")
loop = asyncio.get_event_loop()
print(f"{time.strftime('%H:%M:%S')} 开始调用协程任务")
start = time.time()
loop.run_until_complete(coroutine)
end = time.time()
print(f"{time.strftime('%H:%M:%S')} 结束调用协程任务, 耗时{end - start} 秒")
案例2:
为任务绑定回调函数
import asyncio
import time
async def _task():
print(f"{time.strftime('%H:%M:%S')} task 开始 ")
time.sleep(2)
print(f"{time.strftime('%H:%M:%S')} task 结束")
return "运行结束"
def callback(task):
print(f"{time.strftime('%H:%M:%S')} 回调函数开始运行")
print(f"状态:{task.result()}")
coroutine = _task()
print(f"{time.strftime('%H:%M:%S')} 产生协程对象 {coroutine},函数并未被调用")
task = asyncio.ensure_future(coroutine) # 返回task对象
task.add_done_callback(callback) # 为task增加一个回调任务
loop = asyncio.get_event_loop()
print(f"{time.strftime('%H:%M:%S')} 开始调用协程任务")
start = time.time()
loop.run_until_complete(task)
end = time.time()
print(f"{time.strftime('%H:%M:%S')} 结束调用协程任务, 耗时{end - start} 秒")
二. 并发
如果需要执行多次协程任务并尽可能的提高效率,这时可以定义一个task列表,然后使用asyncio的wait()方法执行即可。
import asyncio
import time
async def task():
print(f"{time.strftime('%H:%M:%S')} task 开始 ")
# 异步调用asyncio.sleep(1):
await asyncio.sleep(2)
# time.sleep(2)
print(f"{time.strftime('%H:%M:%S')} task 结束" )
# 获取EventLoop:
loop = asyncio.get_event_loop()
# 执行coroutine
tasks = [task() for _ in range(5)]
start = time.time()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
end = time.time()
print(f"用时 {end-start} 秒")
三. 异步请求
先启动一个简单的web服务器
from flask import Flask
import time
app = Flask(__name__)
@app.route('/')
def index():
time.sleep(3)
return 'Hello World!'
if __name__ == '__main__':
app.run(threaded=True)
案例1:请求串行走下来,没有实现挂起。
import asyncio
import requests
import time
start = time.time()
async def request():
url = 'http://127.0.0.1:5000'
print(f'{time.strftime("%H:%M:%S")} 请求 {url}')
response = requests.get(url)
print(f'{time.strftime("%H:%M:%S")} 得到响应 {response.text}')
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(f'耗时 {end - start} 秒')
使用await将耗时等待的操作挂起,让出控制权。
当协程执行时遇到await, 时间循环就会将本协程挂起,转而去执行别的协程,知道其他的协程挂起或者执行完毕。
案例2:异步IO请求实例
将请求页面的代码封装成一个coroutine对象,在requests中尝试使用await挂起当前执行的I/O.
import asyncio
import requests
import time
async def get(url):
return requests.get(url)
async def request():
url = "http://127.0.0.1:5000"
print(f'{time.strftime("%H:%M:%S")} 请求 {url}')
response = await get(url)
print(f'{time.strftime("%H:%M:%S")} 得到响应 {response.text}')
start = time.time()
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(f"耗时 {end - start} 秒")
上面的带动并未达到预期的并发效果。原因是requests不是异步请求,无论如何改封装都无济于事,因此需要找真正的IO请求,aiohttp是一个支持异步请求的库,可以用它和anyncio配合,实现异步请求操作。
案例3:使用aiohttp库
import asyncio
import aiohttp
import time
now = lambda: time.strftime("%H:%M:%S")
async def get(url):
session = aiohttp.ClientSession()
response = await session.get(url)
result = await response.text()
await session.close()
return result
async def request():
url = "http://127.0.0.1:5000"
print(f"{now()} 请求 {url}")
result = await get(url)
print(f"{now()} 得到响应 {result}")
start = time.time()
tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
end = time.time()
print(f"耗时 { end - start } 秒")
运行结果符合预期要求,耗时由15秒变成了3秒,实现了并发访问。
将任务数5改成100,运行时间也在3秒多一点,多出来的时间就是I/O时延了。
可见,使用异步协程之后,几乎可以在相同时间内实现成百上千次的网络请求。