【python】- 协程函数 asyncio

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:本文主要提到python中异步并发操作,关于Thread多线程同步并发见上文。

Python 中的协程函数也就是我们所说的异步编程,它允许你编写非阻塞的代码,特别适用于 I/O 密集型任务(如网络请求、文件读写等)。首先需要清楚的明白如下概念:

  1. 同步编程
    像我们正常定义的函数就是同步函数,它只能每个函数依次进行,假如某个函数遇到了阻塞,那么整个程序将会被堵塞住,影响执行效率。当然我们可以通过多线程的方式处理,多线程处理的函数也是同步函数,他们将会在同一时刻同步并发
  2. 异步编程
    异步编程也就是我们今天说的协程,它是在一个单一线程里执行的。它的概念是同一个时间段完成多个任务(就像我们人类一样,需要执行吃饭任务,在点完外卖后,不会一直等待外卖,而是这个时间去打一把游戏,等外卖到了,再继续执行吃饭任务,显然这更加的合理。)这就是所谓的异步并发

提示:以下是本篇文章正文内容,仅供学习参考

一、asyncio是什么?

asyncio 是 Python 标准库中的一个模块,用于编写异步 I/O 操作的代码。它基于事件循环(Event Loop)和协程(Coroutine),能够高效地处理 I/O 密集型任务(如网络请求、文件读写等),同时避免阻塞主线程。

二、使用步骤

1.引入库

代码如下(示例):

import asyncio

2. 定义协程函数 async

代码如下(示例):

import asyncio
import time


async def task_1():
    print("send a task1 request...")
    await asyncio.sleep(5)
    print("get responds")
    return "task1"


async def task_2():
    print("send a task2 request...")
    await asyncio.sleep(3)
    print("get responds")
    return "task2"


if __name__ == '__main__':
    t1 = task_1()
    t2 = task_2()
    print(f"t1: {t1}, t2: {t2}")

async 关键字定义的函数为协程函数,返回一个协程对象,执行结果打印如下,得到的t1t2是协程对象,结果如下

在这里插入图片描述

3. 执行协程函数 await

await 关键字是用来执行协程函数对象的,await暂停执行当前协程直到等待的协程执行完毕,并且让出控制权给事件循环,见示例代码:

import asyncio
import time


async def task_1():
    print("send a task1 request...")
    await asyncio.sleep(2)
    print("get task1 responds")
    return "task1"


async def task_2():
    print("send a task2 request...")
    await asyncio.sleep(3)
    print("get task2 responds")
    return "task2"


def task3():
    print("running task 3")
    time.sleep(3)
    print("end task3")


async def _main():
    ret = await task_1()
    ret = await task_2()
    task3()


if __name__ == '__main__':
    start_ = time.perf_counter()
    asyncio.run(_main())
    end_ = time.perf_counter()
    print(f"total : {end_ - start_}")


执行结果如下:

在这里插入图片描述

从输出结果看任务是顺序执行,运行时间也是接近8 s,那是因为await 的作用是等待后面协程执行结束,在示例代码中task1()、task2() 依次执行,直到结束才会执行下一个,也就是说在此的任务是串行的,适合前后任务有依赖的场景。

获取你会感到疑惑,如果止步于此,为何要大费周章的引入异步函数呢,普通函数不是也是逐行执行吗?await 和普通函数的执行方式确实有一些相似之处,但它们的关键区别在于是否会阻塞事件循环,以及 是否允许并发执行其他任务。我们对上面代码做如下修改:

import asyncio
import time


async def task_1():
    print("send a task1 request...")
    await asyncio.sleep(2)
    print("get task1 responds")
    return "task1"


async def task_2():
    print("send a task2 request...")
    await asyncio.sleep(3)
    print("get task2 responds")
    return "task2"


def task3():
    print("running task 3")
    time.sleep(3)
    print("end task3")


async def _main():
    task1 = asyncio.create_task(task_1())
    task2 = asyncio.create_task(task_2())
    await task1
    await task2
    task3()


if __name__ == '__main__':
    start_ = time.perf_counter()
    asyncio.run(_main())
    end_ = time.perf_counter()
    print(f"total : {end_ - start_}")

运行结果如下:

在这里插入图片描述

可见运行时间变短了,因为task1task2是异步并发的,代码中通过create_task方法创建任务,再用await关键字,task2并没有等待task1执行完毕再执行,两者是并发关系。

这里的执行顺序如下:

  1. 执行task1遇到await asyncio.sleep(2),暂停执行, 把控制权给事件循环。
  2. 事件循环开始执行task2也遇到await asyncio.sleep(3), 暂停执行,交出控制权。
  3. 事件循环发现task1await asyncio.sleep(2)结束了,恢复运行task1
  4. 事件循环再恢复运行task2, 最后再正常等待两个任务执行完毕

所以如上场景适用于task1task2没有依赖,做到了并发的操作,所以await的核心就是让出控制权给事件循环,让事件循环调度所有的协程函数。

注意:同样的我们还可以在task1中调用协程任务task4, task5, 同样遵循上面的规律串行或并行。对于task3,事件循环也会等待task1和task2执行完毕后再继续执行。

对于并行所有任务还有另外一种简单的写法,效果是一样的如下:

async def _main():
    await asyncio.gather(task_1(), task_2())
    task3()

4. 事件循环 (Event Loop)

事件循环(Event Loop) 是异步编程的核心机制,它负责调度和执行异步任务(如协程),并管理 I/O 操作、定时器、回调等。事件循环使得程序能够在等待 I/O 操作(如网络请求、文件读写)时不会阻塞,从而高效地处理并发任务。

事件循环主要作用是负责任务的调度,开启,停止,恢复,关闭,这些操作我们都可以通过asyncio.run(_main()),这里的_main是调度的函数。如上面的代码,就是通过这种方式创建事件循环的。

除了通过asyncio.run的方式自动创建和关闭事件循环,还可以手动创建事件循环,如下:

import asyncio


async def task1():
    print("Task 1 started")
    await asyncio.sleep(1)
    print("Task 1 finished")


async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")


# 获取事件循环
loop = asyncio.get_event_loop()
try:
    # 运行任务
    loop.run_until_complete(task1())
    loop.run_until_complete(task2())
finally:
    loop.close()


稍加修改同步处理任务

import asyncio


async def task1():
    print("Task 1 started")
    await asyncio.sleep(1)
    print("Task 1 finished")


async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")


# 获取事件循环
loop = asyncio.get_event_loop()
try:
    # 运行任务
    loop.run_until_complete(asyncio.gather(task1(), task2()))
finally:
    loop.close()

除了通过asyncio.run自动创建和关闭事件循环,还可以手动管理,代码如下:

import asyncio


async def task1():
    print("Task 1 started")
    await asyncio.sleep(1)
    print("Task 1 finished")


async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")


# 创建事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
    # 运行任务
    loop.run_until_complete(asyncio.gather(task1(), task2()))

finally:
    loop.close()

asyncio.new_event_loop() 创建一个事件循环,然后事件循环运行一个任务,最后关闭事件循环。当然还可以直接获得一个当前的事件循环,假如当前没有事件循环,就会自动创建,代码如下:

import asyncio


async def task1():
    print("Task 1 started")
    await asyncio.sleep(1)
    print("Task 1 finished")


async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")


# 创建事件循环
loop = asyncio.get_event_loop()
try:
    # 运行任务
    loop.run_until_complete(asyncio.gather(task1(), task2()))

finally:
    loop.close()

注意: 通常一个线程里面只允许存在一个事件循环,假如存在多个事件循环运行,会抛出异常。

另外,在一个事件循环里面,开始运行了一个任务后,需要等待这个任务执行结束,才可以重新调用 loop.run_until_complete, 这个函数是不允许嵌套使用的,否则会出现loop is running 的错误信息,如下面代码:

import asyncio


async def task1():
    loop = asyncio.get_event_loop()

    async def task3():
        print("task3")
    task3 = loop.create_task(task3())
    loop.run_until_complete(task3)


async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")


async def _main():
    await task1()
    await task2()


# 创建事件循环
if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    _main = loop.create_task(_main())
    loop.run_until_complete(_main)
    loop.close()

输出报错信息:

在这里插入图片描述

5. 多线程中使用协程 run_coroutine_threadsafe()

前面说了,协程是为单线程服务的,每一个线程中只存在一个事件循环,当然也是可以在多线程里启动多个事件循环的,见如下代码:

import asyncio
import threading
import time


# 定义一个协程任务
async def task(name):
    print(f"{name} 开始")
    await asyncio.sleep(1)
    print(f"{name} 结束")


# 启动事件循环的函数
def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()


# 创建两个事件循环
loop1 = asyncio.new_event_loop()
loop2 = asyncio.new_event_loop()

# 创建两个线程,每个线程运行一个事件循环
thread1 = threading.Thread(target=start_loop, args=(loop1,), daemon=True)
thread2 = threading.Thread(target=start_loop, args=(loop2,), daemon=True)

# 启动线程
thread1.start()
thread2.start()

# 在主线程中调度任务到不同线程的事件循环
asyncio.run_coroutine_threadsafe(task("任务 1"), loop1)
asyncio.run_coroutine_threadsafe(task("任务 2"), loop2)

# 主线程等待一段时间,确保子线程中的任务完成
time.sleep(3)

上面代码在主线程里创建两个事件循环,然后创建两个守护线程,然后再把任务通过run_coroutine_threadsafe()执行,注意事件循环会一直运行,但是由于守护线程的因素,当程序运行结束,整个线程退出。

我们也可以接收run_coroutine_threadsafe()的返回对象,获取任务运行结果。

import asyncio
import threading

async def task(name, delay):
    return f"{name} 结果"

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

# 创建事件循环并启动线程
loop = asyncio.new_event_loop()
thread = threading.Thread(target=start_loop, args=(loop,), daemon=True)
thread.start()

# 调度任务并获取结果
future = asyncio.run_coroutine_threadsafe(task("任务 1", 1), loop)
print(future.result())  # 阻塞等待任务完成并获取结果

注意:这里的future.result()方法是阻塞的,只有当结果返回后程序才会结束,这也保证了线程安全的执行完。

另外还可以通过future.done()判断任务执行状态,代码如下:

# 调度任务并获取结果
future = asyncio.run_coroutine_threadsafe(task("任务 1", 1), loop)
while not future.done():
    print("任务正在执行")
print(future.result())  # 阻塞等待任务完成并获取结果

还可以调用强行停止事件循环方法call_soon_threadsafe,此时不管协程是否执行结束,事件循环都会被关闭:

import asyncio
import threading
import time


async def task(name, delay):
    print(f"{name} 开始")
    await asyncio.sleep(delay)
    print(f"{name} 结束")

def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

# 创建事件循环并启动线程
loop = asyncio.new_event_loop()
thread = threading.Thread(target=start_loop, args=(loop,))
thread.start()

# 调度任务
asyncio.run_coroutine_threadsafe(task("任务 1", 3), loop)

time.sleep(5)
# 停止事件循环
loop.call_soon_threadsafe(loop.stop)

# 等待线程结束
thread.join()


总结

以上就是今天要讲的内容,本文介绍了协程函数和事件循环的使用,对于绝大部分场合,使用ayncio.run()管理事件循环是最好的选择。

### 协程的概念 Python 协程是一种轻量级的并发编程模型,它允许函数在执行过程中暂停并保存当前状态,以便稍后从暂停的位置继续执行。这种特性使得协程非常适合处理高并发和 I/O 密集型任务,因为它们能够在单线程中通过非阻塞的方式进行任务调度,减少了线程上下文切换的开销,从而更有效地利用计算资源 [^1]。 在 Python 中,协程通常通过 `async def` 定义,并使用 `await` 表达式来挂起和恢复执行。例如: ```python async def my_coroutine(): print("Coroutine started") await asyncio.sleep(1) print("Coroutine finished") ``` ### 协程与线程的区别 #### 1. **实现方式** - **线程**:线程是操作系统级别的并发机制,每个线程都有自己的栈空间和寄存器状态,线程之间的切换由操作系统调度器负责。线程的创建和销毁成本较高,且线程间的上下文切换会带来一定的开销 [^2]。 - **协程**:协程是用户级别的并发机制,它们的调度完全由程序控制,而不是操作系统。协程的切换不需要操作系统介入,因此切换成本更低,创建和销毁的开销也较小 [^3]。 #### 2. **资源占用** - **线程**:由于线程是由操作系统管理的,每个线程都需要分配一定的内存空间(如栈空间),这使得线程的资源占用相对较大。当线程数量较多时,可能会导致内存不足的问题 [^2]。 - **协程**:协程的资源占用非常小,通常只需要几百字节的内存。这使得在同一时间内可以创建成千上万个协程而不会消耗过多的系统资源 [^1]。 #### 3. **并发模型** - **线程**:线程的并发模型基于抢占式调度,操作系统会根据优先级和时间片来决定哪个线程获得 CPU 时间。这种模型可能导致竞态条件和死锁等问题,需要通过锁机制来保证线程安全 。 - **协程**:协程的并发模型基于协作式调度,协程之间的切换是由程序显式控制的。这意味着协程在同一时刻只有一个在运行,避免了多线程中的竞态条件问题,简化了并发编程的复杂性 [^3]。 #### 4. **适用场景** - **线程**:线程适用于需要充分利用多核处理器的 CPU 密集型任务。由于线程可以并行执行,因此在处理计算密集型任务时,线程可以提供更好的性能 [^2]。 - **协程**:协程更适合处理 I/O 密集型任务,如网络请求、文件读写等。在这种场景下,协程可以通过异步 I/O 操作来避免阻塞,从而提高程序的整体吞吐量 [^1]。 #### 5. **编程模型** - **线程**:线程的编程模型较为复杂,需要处理线程同步、资源共享等问题。Python 中的 `threading` 模块提供了线程的支持,但由于全局解释器锁(GIL)的存在,Python 的多线程并不能真正实现并行执行 [^2]。 - **协程**:协程的编程模型相对简单,主要依赖于 `asyncio` 库提供的事件循环和异步 I/O 操作。通过 `async/await` 语法,开发者可以更容易地编写异步代码 。 ### 总结 Python 协程是一种轻量级的并发编程模型,它通过协作式调度和用户级别的上下文切换,减少了线程上下文切换的开销,从而更有效地利用计算资源。与线程相比,协程的资源占用更小,适合处理 I/O 密集型任务,而线程更适合处理 CPU 密集型任务。理解它们的关系与差异有助于在实际应用中选择合适的并发模型 [^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值