【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()管理事件循环是最好的选择。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值