python协程详细讲解

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)创建一个taskrun_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())
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值