python的线程是由操作系统调度,由于GIL锁的存在,python操作线程时只能操作单个线程,当单个线程遇到IO阻塞或执行时间较长,则会被迫交出cpu的控制权,转交给其他线程处理,协程是存在单线程里的,是由应用程序控制的,跟线程一样,遇到IO阻塞或执行时间较长,会被迫交出程序的控制权,切换到其他协程,切换的开销比线程的切换开销小,这种机制能够提高效率。
协程本质上是单线程,协程的调度是在单个线程里执行的,切换的开销比较小,因此效率上略微比多线程高。有了协程,在执行IO耗时操作时,函数可以临时让出控制权,让CPU去执行其他函数。
协程的应用场景:IO密集型。
python3.4中使用协程的库:asyncio
1. 协程的简单实例
import asyncio
import time
async def task():
print("task start")
time.sleep(2)
print("task end")
coroutine = task() # 返回coroutine对象,方法没有执行
print("创建coroutine对象,方法task没有执行")
loop = asyncio.get_event_loop() # 获取事件循环loop对象
start = time.time()
loop.run_until_complete(coroutine) # 将协程对象注册到事件循环loop中并启动
print(f"调用协程任务,耗时{time.time() - start} 秒")
输出:
创建coroutine对象,方法task没有执行
task start
task end
耗时2.0004966259002686 秒
说明:在方法task()前面使用关键字async,声明该方法是一个协程,然后直接调用该方法,但是该方法并没有立即执行,而是返回了一个coroutine对象。使用get_event_loop()方法获取一个事件循环loop对象,并调用loop对象的run_util_complete()方法把协程对象注册到事件循环中,并启动task方法。async定义的方法无法直接执行,必须将其注册到事件循环中才可以执行。
2. 给协程方法绑定回调函数
import asyncio
import time
async def _task():
print("task start")
time.sleep(2)
print("task end")
return "_task方法返回的结果"
def callback(task):
"""
:param task: 协程任务对象
:return:
"""
print("回调函数开始运行")
print(f"状态:{task.result()}")
coroutine = _task()
print("创建coroutine对象,方法task没有执行")
task = asyncio.ensure_future(coroutine) # 返回task对象
task.add_done_callback(callback) # 添加回调函数
loop = asyncio.get_event_loop()
print("开始调用协程任务")
loop.run_until_complete(task)
print("结束调用协程任务")
输出:
创建coroutine对象,方法task没有执行
开始调用协程任务
task start
task end
回调函数开始运行
状态:_task方法返回的结果
结束调用协程任务
3. 协程的并发
以上两个实例只执行了一个任务,现在演示下执行多个IO密集型任务。
需求说明:定义一个task耗时任务列表,使用协程并发执行列表中的任务。await关键字可以挂起一个函数或方法。
import asyncio
import time
async def task():
print("task start")
# 异步调用asyncio.sleep(1)等待
await asyncio.sleep(2)
print("task end" )
# 获取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} 秒")
输出:
task start
task start
task start
task start
task start
task end
task end
task end
task end
task end
用时 2.000945806503296 秒
说明:首先定义个耗时2秒的函数,这里我们使用了await关键字,根据官方文档说明,await关键字后面的对象必须是如下类型之一。
- 一个原生的coroutine对象。
- 一个由type.coroutine()修饰的生成器,这个生成器可以返回coroutine对象。
- 一个包含await方法的对象返回的迭代器
asynicio.sleep(2)是一个由coroutine修饰的生成器对象,表示等待2秒,是异步的,接下来我们定义了一个task列表,由5个task()组成,输出的耗时接近于单个任务的耗时,最后使用loop.run_until_complete(asyncio.wait(tasks))提交执行,从而实现并发操作。
4.协程的异步请求
IO密集型任务常见的应用有网络请求、文件读取等,模拟异步请求之前,我们本地起了一个flask的web服务器。
flask的安装:pip3 install flask
4.1 启动一个简单的Flask 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)
输出:
* Serving Flask app "coroutine_flask_demo" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
说明: 在浏览器中输入:http://127.0.0.1:5000/ ,3秒后页面会显示Hello World!。注意:服务器启动时,run()方法中设置参数threaded=True,表示Flask启动了多线程、多进程。否则的话默认是只有一个线程的,如果设置此参数开启多线程,那么同一时刻遇到对个请求时,也只能一个个排队等待,瓶颈就会出现在服务端。所以简单的flask服务设置threaded=True是很有必要的,如果复杂的项目,推荐使用nginx+uwsgi来处理请求并发。
4.2 协程异步请求的实现
注意:使用aiohttp来发起异步请求,我们常用的request是同步的。
以下代码通过aiohttp的ClientSession类的get()方法进行100次并发请求
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() # 获取响应内容
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(100)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
print(f"耗时 { time.time() - start } 秒")
输出:
耗时 3.0944957733154297 秒
说明:通过输出可以看到使用协程进行100次异步请求的耗时接近一次的请求的耗时。