1. 协程介绍
首先,在学习协程之前我们需要了解协程是什么。
协程(Coroutine),也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块相互切换执行,即在同一线程内,一段执行代码过程中,可以中断并跳转到另一段代码中,接着之前中断的地方继续执行。
协程与多线程的状态类似,但是,其最大的不同在于协程使用的只有一个线程的资源。
那么,协程的作用是什么呢?为什么需要中断一段代码跳到另一部分代码呢?
简而言之,协程的意义在于:在一个线程中如果遇到IO等待时间,线程不会傻等,利用空闲的时候再去干点其他事。
我们知道,如果一个线程仅执行CPU中的资源的话,其速度是很快的,但是,如果执行过程中需要用到IO资源,那么内存就需要与磁盘进行数据的传输,这个过程比内存中数据的执行慢得多,这也就导致如果需要执行完程序块1再执行完程序块2,那么CPU就得等待程序块1中的内存与磁盘数据传输,而这段时间CPU是空闲的,这显然造成了CPU资源的浪费,于是,协程的作用就凸显出来了。有了协程,我们可以使程序块1执行到内存与磁盘数据传输的时候,再去执行程序块2,这样,内存的数据传输与CPU都可以利用起来,程序的执行显然就更加有效率。
综上所述,我们可以提取到,协程优点:
- 无需线程上下文切换,协程避免了无意义的调度,可以提高性能。
- 无需原子操作锁定及同步开销。
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本,一个CPU支持上万的协程不是问题,很适合用于高并发处理。
协程缺点:
- 无法利用多核资源。协程的本质是单线程,不能同时将单个CPU的多个核用上,协程需要进程配合才能运行在多CPU上。
- 进行阻塞操作(如IO时)会阻塞掉整个程序
2. 事件循环
事件循环是协程中的一个重要概念,协程正是依据事件循环来判断其中的任务是否已经完成,并且遇到阻塞任务时执行其他的任务。
我们可以将其理解成一个死循环,伪代码表示如下:
# 伪代码
任务列表 = [任务1、任务2....]
while True:
可执行的任务列表, 执行完成的任务列表 = 去任务列表中检查所有的任务, 将“可执行”和“已完成”的任务返回
for 就绪任务 in 可执行任务列表:
执行就绪任务
for 已完成的任务 in 执行完成的任务列表:
任务列表中移除 已完成的任务
如果 任务列表 中任务都完成,则跳出循环
在python中,我们可以使用如下的代码创建任务循环,在这里只需要关注事件循环即可,相关的函数和关键字等后续会进行讲解,代码如下:
import asyncio
# 异步任务1
async def work_1():
for _ in range(5):
print('我是异步任务1')
await asyncio.sleep(1)
# 异步任务2
async def work_2():
for _ in range(5):
print('我是异步任务2')
await asyncio.sleep(1)
# 将多个任务存放在一个列表中
tasks = [
work_1(),
work_2()
]
# 创建或获取一个事件循环
loop = asyncio.get_event_loop()
# 将任务列表放到事件循环中并开始执行
loop.run_until_complete(asyncio.wait(tasks))
在python3.7后,我们无需手动创建事件循环,使用 asyncio.run
即可达到相同的效果,即可以将最后两句代码替换为下面的代码也可以达到相同的效果:
# 创建事件循环并执行
asyncio.run(asyncio.wait(tasks))
但是需要注意的是原理都是一样的。
3. 协程编程
3.1 基本概念
协程函数:指定义函数的时候,前面加上 async
关键字,即定义为 async def func()
的函数为协程函数。
协程对象:指执行协程函数所创建的协程对象。
async def func():
print("这里是函数func")
# 创建协程对象1
func()
# 创建协程对象2
r = func()
执行上面的代码,就相当于创建了一个协程对象,需要注意的是,执行协程函数创建写成对象,即运行上述代码,函数内部的代码是不执行的。
如果想要运行协程函数内部的代码,必须要将协程对象交给事件循环来进行处理,代码如下:
import asyncio
async def func():
print("这里是函数func")
r = func()
asyncio.run(r)
3.2 await 关键字
关于await关键字有三个需要注意的点:
- await关键字后面跟的是可等待的对象(如协程对象,Future对象,Task对象),所以如果想要暂停一秒,不能使用
await time.sleep(1)
,因为time.sleep(1)
不是可等待对象 - await可以获取到可等待对象的返回值
- 当事件循环遇到await关键字之后会阻塞当前代码,并调度其他任务。
示例1:
import asyncio
async def others():
print("start")
await asyncio.sleep(2)
print("end")
return "返回值你猜"
async def func():
print("执行协程内部代码")
# 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)
# 获取返回值
response = await others()
print("IO结束,结果为{}".format(response))
asyncio.run(func())
示例2:
import asyncio
async def others():
print("start")
await asyncio.sleep(5)
print("end")
return "返回值你猜"
async def func():
print("执行协程内部代码")
# 获取返回值
response1 = await others()
response2 = await others()
asyncio.run(func())
大家猜猜上面的获取 response1, response2
两个返回值的过程是串行的还是并行的?答案是串行的。
因为 await遇到耗时任务会阻塞主程序的代码,也就是说该程序的 response2 = await others()
就根本没有执行到就被阻塞了,所以只能是串行的,那么,有没有什么办法能够让其能够使用协程的方法运行呢?
3.3 task对象
task
对象就是往事件循环中加入任务用的。Task
用于开发调度协程,通过asyncio.create_task(协程对象)
创建(python3.7
之后有这个函数),也可以用asyncio.ensure_future(coroutine)
和 loop.create_task(coroutine)
创建一个task
。run_until_complete
的参数是一个future
对象,当传入一个协程,其内部会自动封装成task
。不建议手动实例化task
对象。
于是,将上面的例子改为协程方式运行,代码为:
import asyncio
async def others():
print("start")
await asyncio.sleep(5)
print("end")
return "返回值你猜"
async def func():
print("执行协程内部代码")
# 创建并提交任务到事件循环
task_1 = asyncio.create_task(others())
task_2 = asyncio.create_task(others())
response_1 = await task_1
print('task_1的IO操作结果为:', response_1)
response_2 = await task_2
print('task_2的IO操作结果为:', response_2)
asyncio.run(func())
打印结果如下:
执行协程内部代码
start
start
end
end
task_1的IO操作结果为: 返回值你猜
task_2的IO操作结果为: 返回值你猜
上述代码已经能够很好的将任务提交到事件循环中了,但是,如果我们需要创建100个任务提交到事件循环呢,难道我们要手动创建100个对象?
其实用的更多的方式是将任务全都写到一个任务列表中去,具体代码示例如下:
import asyncio
async def work(x):
print("当前接收的参数为:{}".format(x))
await asyncio.sleep(x)
return "返回值为:{}".format(x)
async def func():
print("执行协程内部代码")
# 获取返回值
task_list = [asyncio.create_task(work(i)) for i in range(10)]
# 不能直接使用 await task_list,await后面只能跟可等待对象
# done是已完成任务的返回值,pending是未完成的任务,asyncio.wait(task_list, timeout = 2)表示执行两秒,就可能产生未完成任务
done, pending = await asyncio.wait(task_list)
print(done)
print(pending)
asyncio.run(func())
上述代码获取任务列表中的返回值主要有以下两种方式:
方式1:
for item in done:
print(item.result())
输出如下:
返回值为:5
返回值为:2
返回值为:9
返回值为:6
返回值为:3
返回值为:0
返回值为:7
返回值为:4
返回值为:1
返回值为:8
方式2:
res = await asyncio.gather(*task_list)
print(res)
输出如下:
[‘返回值为:0’, ‘返回值为:1’, ‘返回值为:2’, ‘返回值为:3’, ‘返回值为:4’, ‘返回值为:5’, ‘返回值为:6’, ‘返回值为:7’, ‘返回值为:8’, ‘返回值为:9’]
可以发现,使用 wait
方法得到的返回值是无序的,使用 gather
方法得到的返回值是有序的。
3.4 future对象
future对象用于表示尚未完成的操作的结果,它允许在不阻塞主线程的情况下等待操作的完成,Future 可能代表一个已经完成的值或一个将来可能完成的值。在这里我们仅讲述 asyncio
中的 Future对象。
Task继承Future,Task对象内部await结果的处理基于Future对象来的。
当创建了一个 Future 对象或者 Task 对象后,可以使用 await 关键字来等到Future对象的结果,如下代码:
async def my_coroutine():
future = asyncio.Future()
# 在协程中,可以等待 Future 对象的结果
result = await future
print("Result:", result)
asyncio.run(my_coroutine())
上面代码创建了一个future对象,但是里面没有具体的行为,上述代码等到future代码执行就相当于是一个无限等待的过程,即死循环。
当然,我们也可以用 future.set_result(r)
来设置future的运行结果为 r
,设置后,可以使用 future.result()
来获取future的结果。
其实,一般情况下我们不会显示的创建future对象,我们创建的都是Task对象,利用Task对象来完成协程的相关任务。
3.5 异步迭代器
实现了__aiter__()
和__anext__()
方法的对象,而__anext__()
必须返回一个awaitable
对象,async for
会处理异步迭代器的__anext__()
方法所返回的可等待对象,直到其引发一个StopAsyncIteration
异常。
异步迭代对象指可在async for
语句中被使用的对象,必须通过它的__aiter__()
方法返回一个asynchronous iterator
import asyncio
class Reader:
"""
自定义异步迭代器(同时也是异步可迭代对象)
"""
def __init__(self):
self.count = 0
async def readline(self):
# await asyncio.sleep(1)
self.count += 1
if self.count == 100:
return None
return self.count
def __aiter__(self):
return self
async def __anext__(self):
value = await self.readline()
if value == None:
raise StopAsyncIteration
return value
上述代码不能直接使用async for
执行,如果非要执行的话,会进行报错,如下:
async for i in Reader():
print(i)
打印的报错结果:
File "/workspace/python_code/协程备课.py", line 24
async for i in Reader():
^
SyntaxError: 'async for' outside async function
async for
必须运行在一个协程函数内
async def main():
async for i in Reader():
print(i)
asyncio.run(main())
3.6 异步上下文管理
异步上下文管理器是一种用于协程(asyncio)的特殊类型的上下文管理器,用于管理异步资源的分配和释放。异步上下文管理器通常与 async with
语句一起使用,以确保在异步代码块执行前分配资源,并在执行后释放资源。
要创建一个异步上下文管理器,需要定义一个类,该类必须实现两个特殊方法 __aenter__
和 __aexit__
。这些方法允许您定义资源的获取和释放逻辑。
__aenter__
方法:在进入async with
代码块时调用。通常在这里执行资源的分配或初始化操作。__aexit__
方法:在退出async with
代码块时调用。通常在这里执行资源的释放或清理操作。async with
语句必须写在一个协程函数中 ,不能在函数外使用
其代码示例如下:
import asyncio
class MyAsyncContextManager:
async def __aenter__(self):
print("Entering the async context")
return self
async def __aexit__(self, exc_type, exc, tb):
"""
with语句运行结束之后触发此方法的运行
exc_type:如果抛出异常, 这里获取异常类型
exc_val:如果抛出异常, 这里显示异常内容
exc_tb:如果抛出异常, 这里显示所在位置, traceback
"""
print("Exiting the async context")
async def main():
async with MyAsyncContextManager() as manager:
print("Inside the async context")
if __name__ == "__main__":
asyncio.run(main())
3.7 异步示例:MySQL读取
首先,我们创建一个异步上下文管理器来管理连接和关闭MySQL数据库,然后,使用第三方的 aiomysql
库来实现MySQL的异步管理,代码如下:
import aiomysql
import asyncio
# 定义异步上下文管理器
class MySQLDB:
def __init__(self, host, port, user, password, db):
self.host = host
self.port = port
self.user = user
self.password = password
self.db = db
self.pool = None
async def __aenter__(self):
# 异步上下文管理器的进入方法,在这里建立数据库连接
self.pool = await aiomysql.create_pool(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
db=self.db,
autocommit=True, # 设置自动提交事务
cursorclass=aiomysql.cursors.DictCursor # 结果以字典形式返回
)
return self.pool
async def __aexit__(self, exc_type, exc_val, exc_tb):
# 异步上下文管理器的退出方法,在这里关闭数据库连接
self.pool.close()
await self.pool.wait_closed()
# 使用异步上下文管理器
async def query_data():
# 使用MySQLDB异步上下文管理器来建立数据库连接并执行查询操作
async with MySQLDB(host='your_host',
port=3306,
user='your_user',
password='your_password',
db='your_db') as pool:
# 从连接池中获取连接
async with pool.acquire() as conn:
# 使用连接创建游标
async with conn.cursor() as cursor:
# 执行SQL查询语句
await cursor.execute("SELECT * FROM your_table")
# 获取查询结果
result = await cursor.fetchall()
return result # 返回查询结果
# 异步函数示例
async def main():
data = await query_data()
print(data)
# 运行异步函数
if __name__ == "__main__":
asyncio.run(main())