Python中异步编程的9个级别

📝 面试求职: 「面试试题小程序」 ,内容涵盖 测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、性能测试、计算机网络知识、Jmeter、HR面试,命中率杠杠的。(大家刷起来…)

📝 职场经验干货:

软件测试工程师简历上如何编写个人信息(一周8个面试)

软件测试工程师简历上如何编写专业技能(一周8个面试)

软件测试工程师简历上如何编写项目经验(一周8个面试)

软件测试工程师简历上如何编写个人荣誉(一周8个面试)

软件测试行情分享(这些都不了解就别贸然冲了.)

软件测试面试重点,搞清楚这些轻松拿到年薪30W+

软件测试面试刷题小程序免费使用(永久使用)


对于初学者来说,异步编程似乎很复杂且令人望而生畏。但它是你可以添加到 Python 工具包的最强大的工具之一。

想象一下,编写的代码在等待响应时永远不会闲着——你的程序会变得更快、响应更快,并且能够同时处理多个任务。

在本文中,我将带大家逐步了解从基础到高级并发技术的9个级别。无论你是异步新手还是希望提高技能,希望对大家有所帮助。

Level 0:理解异步编程的必要性

考虑一个从多个网站获取数据的脚本。使用同步编程,每个请求都会阻止程序,直到完成为止:

import requests
import time

# The urls list could be much longer
urls = ["http://example.com",
        "http://example.org",
        "http://example.net/",]

start_time = time.time()

for url in urls:
    response = requests.get(url)
    print(response.status_code)

print(f"Sync code cost {time.time() - start_time:.2f} seconds")
# Sync code cost 0.64 seconds

上述代码以同步方式逐一处理 3 个 URL,按顺序处理每个 URL,然后转到下一个,直到当前 URL 完成。

总共花费了0.64秒。

看上去可以接受吗?

想象一下,10 个 URL 需要等待 3 秒钟,最终整个过程需要 30 秒。更不用说 100 个 URL、100000 个 URL 等等。这个程序会非常耗时。

这种场景,也就是所谓的 I/O 绑定场景,是异步编程的表演时刻。​​​​​​​

import aiohttp
import asyncio
import time

asyncdeffetch_url(url):
    asyncwith aiohttp.ClientSession() as session:
        asyncwith session.get(url) as response:
            print(f"Status: {response.status}")

asyncdefmain():
    urls = ["http://example.com",
            "http://example.org",
            "http://example.net/",]
    start_time = time.time()
    await asyncio.gather(*(fetch_url(url) for url in urls))
    print(f"Async code cost {time.time() - start_time:.2f} seconds")

asyncio.run(main())
# Async code cost 0.22 seconds

以上是实现相同任务的异步版本代码。它仅花费了0.22秒!

为什么以及如何实现这一点?

因为通过采用异步编码技术,可以同时运行多个任务(在这种情况下同时触发所有请求),从而大大减少等待时间。

如果不能完全理解上述异步代码,请不要担心,现在让我们深入研究 Python 异步技术。

Level 1:理解事件循环

Python 中异步编程的核心是事件循环。

我们可以将其视为主调度程序,无需暂停整个程序即可协调任务的执行,因此异步魔法就起作用了。

在底层,事件循环使非阻塞执行成为可能,这意味着当一个任务正在等待(例如,耗时的 I/O 操作)时,其他任务可以继续运行。

非阻塞是异步编程的核心优势。它与同步编程中的阻塞操作形成对比,在同步编程中,整个程序必须等待一个任务完成后才能继续执行下一个任务。这是不必要的时间成本的根源。

Python 的asyncio模块提供了一种实现事件循环的简单方法。事件循环管理每个协程(协程是一种可以暂停和恢复的特殊函数,允许非阻塞操作)的执行和恢复时间,从而确保非阻塞操作。

说起来容易做起来难,让我们看一些代码:​​​​​​​

import asyncio


asyncdeftask_1():
    print("Starting task 1")
    await asyncio.sleep(2) # simulate a slow I/O operation
    print("Task 1 done")

asyncdeftask_2():
    print("Starting task 2")
    await asyncio.sleep(1) # simulate another slow I/O operation
    print("Task 2 done")

asyncdefmain():
    await asyncio.gather(task_1(), task_2())

asyncio.run(main())
# Starting task 1
# Starting task 2
# Task 2 done
# Task 1 done

在上述代码中,`asyncio.run(main())` 启动事件循环,该事件循环管理协程 `task_1` 和 `task_2`。事件循环并发运行它们,使得 `task_2` 能够在 `task_1` 完成之前启动并完成。 

这就是异步程序快得多的原因。事件循环从不等待任何一方。如果存在耗时较长的操作,它就会开始做其他事情,稍后再回来处理这个耗时操作。它总是在努力为你工作呢 :) 

但是,事件循环如何知道何时停止当前协程并跳转到另一个协程呢? 

这就涉及到 `async` 和 `await` 关键字了。

Level 2:巧妙使用 async 和 await

有两个 Python 关键字在异步世界中随处可见——async和await。

  • async用于定义协程,协程是一种可以暂停和恢复而不会阻止其他操作的函数。当你使用async定义函数时,即表示它可用于await将控制权交还给事件循环。

  • await用于在async函数中暂停执行,直到等待的协程完成。await使用时,它会告诉事件循环当前协程正在等待某个结果,从而允许事件循环同时运行其他协程。

因此,借助async和await关键字,事件循环能够知道如何正确处理协程。

事件循环await通过暂停当前协程并继续执行其他任务来协调调用。这确保了对多个协程的高效管理,使异步编程成为处理 I/O 密集型和高延迟操作的理想选择。本质上,async定义了异步行为可以发生的位置,而await指定何时应将控制权交还给事件循环,从而实现高效的多任务处理。

Level 3:像大师一样使用asyncio

前面的例子大家应该都看过了asyncio,模块的作用其实就是 Python 中实现异步程序的核心模块。

让我们回到前面的一个例子进行更深入的探索:​​​​​​​

import asyncio


asyncdeftask_1():
    print("Task 1 started")
    await asyncio.sleep(2)
    print("Task 1 finished")
    return"Result 1"


asyncdeftask_2():
    print("Task 2 started")
    await asyncio.sleep(1)
    print("Task 2 finished")
    return"Result 2"


asyncdefmain():
    await asyncio.gather(task_1(), task_2())


asyncio.run(main())
# Task 1 started
# Task 2 started
# Task 2 finished
# Task 1 finished
asyncio.gather()是同时运行多个任务(协同程序)的常用方法。

它简单明了,但如果你需要对每个任务进行更多单独控制。可以通过以下方式明确管理它们asyncio.create_task():​​​​​​​

import asyncio


asyncdeftask_1():
    print("Task 1 started")
    await asyncio.sleep(2)
    print("Task 1 finished")
    return"Result 1"


asyncdeftask_2():
    print("Task 2 started")
    await asyncio.sleep(1)
    print("Task 2 finished")
    return"Result 2"


asyncdefmain():
    t1 = asyncio.create_task(task_1())
    t2 = asyncio.create_task(task_2())

    # Wait for both tasks to finish
    await t1
    await t2


asyncio.run(main())
# Task 1 started
# Task 2 started
# Task 2 finished
# Task 1 finished

这两种方法的结果是一样的。但是,当以第二种方式执行协程时,可以应用更多的单独控制,例如在任务 1 完成之前将其取消。

Level 4:轻松取消过长的异步任务

等待每个协程完成不一定是最好的解决方案。在某些情况下,你可能希望直接取消太长的任务。可以通过以下cancel()方法直观地完成:​​​​​​​

import asyncio

asyncdeftask_1():
    print("Task 1 started")
    try:
        await asyncio.sleep(2)
    except asyncio.CancelledError:
        print("Task 1 was cancelled")
        raise
    print("Task 1 finished")
    return"Result 1"

asyncdeftask_2():
    print("Task 2 started")
    await asyncio.sleep(1)
    print("Task 2 finished")
    return"Result 2"

asyncdefmain():
    t1 = asyncio.create_task(task_1())
    t2 = asyncio.create_task(task_2())

    # Wait for Task 2 to finish
    await t2

    # Cancel Task 1 before it finishes
    t1.cancel()

    # Wait for Task 1 to handle the cancellation
    try:
        await t1
    except asyncio.CancelledError:
        print("Handled cancellation of Task 1")

asyncio.run(main())
# Task 1 started
# Task 2 started
# Task 2 finished
# Task 1 was cancelled
# Handled cancellation of Task 1

如上所示,任务 1 在完成之前被取消。

注意:方法执行后立即启动了任务 1。是完成任务 1,因此调用asyncio.create_task(task_1())await t1t1.cancel() 的正确时间是在这两个命令之间。

Level 5:超时任务处理asyncio.wait_for()

在某些情况下,取消协程可能太过粗暴,我们应该给予每个协程公平的等待时间来处理,以免等待时间过长。

是的,我们需要的是超时限制,该asyncio.wait_for()方法允许设置协程完成的最大时间限制。​​​​​​​

import asyncio


async def slow_task():
    await asyncio.sleep(5)
    return "Task finished"

async def main():
    try:
        result = await asyncio.wait_for(slow_task(), timeout=2)
        print(result)
    except asyncio.TimeoutError:
        print("Task timed out!")

asyncio.run(main())
# Task timed out!

如上例所示,我们应用该asyncio.wait_for()方法,将最大等待时间设置为 2 秒。慢速任务由于耗时 5 秒而超时。

Level 6:限制并发asyncio.Semaphore To Prevent Resources Overload

异步程序并不总是完美的。有时它们甚至是危险的。

例如,如果你同时运行太多的协程,相对资源将被过度使用,你的服务器就会陷入卡住。

这就是你需要了解asyncio.Semaphore对象的原因。它有助于限制可以同时运行的并发任务的数量,这在访问无法处理无限数量的同时请求的共享资源或外部服务时尤为重要。

例如,下面的程序应用asyncio.Semaphore()技巧来限制同时运行的协程最多为 5 个:​​​​​​​

import asyncio

semaphore = asyncio.Semaphore(5)

async def limited_task(n):
    async with semaphore:
        print(f'Task {n} started')
        await asyncio.sleep(1)
        print(f'Task {n} finished')

async def main():
    tasks = [limited_task(i) for i in range(10)]
    await asyncio.gather(*tasks)

asyncio.run(main())
# Task 0 started
# Task 1 started
# Task 2 started
# Task 3 started
# Task 4 started
# Task 0 finished
# Task 1 finished
# Task 2 finished
# Task 3 finished
# Task 4 finished
# Task 5 started
# Task 6 started
# Task 7 started
# Task 8 started
# Task 9 started
# Task 5 finished
# Task 6 finished
# Task 7 finished
# Task 8 finished
# Task 9 finished
从结果中我们可以清楚的看到,前5个任务是先同时执行的,然后是后5个任务。

这种对资源使用的控制看起来很微妙,但它可以避免大规模生产环境中出现意外问题。

Level 7:异步 Python 代码的错误处理

正确的错误处理是健壮代码的关键。

大多数情况下,异步程序使用与同步代码相同的方法处理错误。

仅对于某些异常,你需要了解并使用特定的异步版本错误对象,例如asyncio.CancelledError。(异步 Python 错误的完整列表在此处。)

除了具体的错误之外,还有一个容易出现 bug 的地方值得一提:

当使用asyncio.create_task()并发运行任务时,我们应该注意错误处理策略。由于任务独立运行,因此必须在任务内部处理异常,或者等待任务并在之后捕获异常。

在生产中,我们可能不知道任务是否有 try-catch。因此最佳做法始终是将其放入await tasktry-catch 结构中,如下所示:​​​​​​​

import asyncio

async def task_1():
    print("Task 1 started")
    await asyncio.sleep(2)
    print("Task 1 finished")
    return "Result 1"

async def main():
    t1 = asyncio.create_task(task_1())
    t1.cancel()
    try:
        await t1
    except asyncio.CancelledError:
        print("Handled cancellation of Task 1")

asyncio.run(main())
# Handled cancellation of Task 1

Level 8:异步队列:更快的生产者 - 消费者模式

队列通常用于生产者-消费者模式。它充当生产者和消费者之间的缓冲区,有助于解耦它们的操作并在整洁的数据结构中管理通信。

Python 有一个内置的队列实现,queue.Queue同时它还提供了一个异步版本—— asyncio.Queue。

它们都应用了队列的思想。但是,异步版本使生产和消费过程都异步化,这可以大大提高代码的性能。

例如,以下代码片段使用异步队列并使用它来处理生产和消费:​​​​​​​

import asyncio


async def producer(queue):
    for i in range(3):
        print(f"Producing {i}")
        await queue.put(i)
        await asyncio.sleep(2)  # simulate a delay for a time-consuming process


async def consumer(queue):
    while True:
        item = await queue.get()
        print(f"Consuming {item}")
        queue.task_done()


async def main():
    queue = asyncio.Queue()
    prod = asyncio.create_task(producer(queue))
    cons = asyncio.create_task(consumer(queue))

    await asyncio.gather(prod)
    await queue.join()
    cons.cancel()


asyncio.run(main())
# Producing 0
# Consuming 0
# Producing 1
# Consuming 1
# Producing 2
# Consuming 2

它演示了一个典型的生产者-消费者场景,其中一个协程(生产者)正在生产产品,而另一个协程(消费者)正在消费它们,两者同时运行。

现在,让我们深入了解异步队列的使用要点:

  • 使用queue = asyncio.Queue()创建队列。

  • producer和consumer任务与asyncio.create_task()同时创建和启动。

  • await asyncio.gather(prod)确保主程序等待生产者完成所有物品的生产。

  • await queue.join()阻塞主函数,直到队列中的所有项目都已处理完毕。它确保程序在退出之前等待消费者消费完所有生成的内容。

  • 每次消费者处理一个项目时,它都会使用queue.task_done()向队列发出信号,表明该项目已完全处理完毕。queue.join()最终允许解除阻塞。

  • 当队列为空并且所有任务完成后,cons.cancel()取消消费者任务以停止其无限循环。

感谢阅读并祝你编码愉快!❤️

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】
在这里插入图片描述
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值