欢迎光临
我们一直在努力

Python异步编程

异步编程,就是程序在等待某件事时,不傻等,而是先去做别的事。

适合:

  • 网络请求

  • 下载文件

  • 查询数据库

  • 调用大模型 API

  • Redis、消息队列

  • WebSocket

  • 同时处理大量连接

因为这些工作主要消耗 CPU,而异步主要解决“等待时间太多”的问题

1. 先理解同步编程

  • 普通 Python 代码默认是同步执行的。

import time

def task(name):
print(f"{name} 开始")
time.sleep(2)
print(f"{name} 结束")

task("任务1")
task("任务2")
task("任务3")

"""
执行过程:

任务1开始
等待2秒
任务1结束

任务2开始
等待2秒
任务2结束

任务3开始
等待2秒
任务3结束

总耗时大约:
2 + 2 + 2 = 6秒
"""

这就像一个人去饭店:

  • 点一道菜
  • 站在厨房门口等菜做好
  • 菜做好后再点下一道
  • 再继续傻等
  • 效率显然不高

    2. 异步编程是什么

    异步的思路是:

  • 任务1开始,发现要等待
  • 先暂停任务1
  • 去执行任务2
  • 任务2也要等待,再去执行任务3
  • 谁先等待结束,就继续执行谁
  • 类似:

    任务1:正在等待网络返回

    任务2:正在等待数据库返回

    任务3:正在等待文件读取 CPU:你们都在等,那我先处理其他任务

    这不是说三个任务真的同时占用三个 CPU。

    而是它们在等待期间互相切换。

    3. 异步编程最基本的三个关键词

    async
    await
    asyncio

    3.1 async def

    普通函数:

    def hello():
    print("你好")

    异步函数:

    async def hello():
    print("你好")

    使用 async def 定义的函数叫:

    • 协程函数

    调用协程函数时,不会马上执行。

    async def hello():
    print("你好")

    result = hello()
    print(result)

    """
    <coroutine object hello at 0x000001DDF55B4AC0>
    sys:1: RuntimeWarning: coroutine 'hello' was never awaited
    """

    报错是因为:

    hello()返回的是一个“协程对象”,不是立即执行函数,需要把它交给事件循环运行。

    3.2 asyncio.run()

    正确执行异步函数:

    import asyncio

    async def hello():
    print("你好")

    asyncio.run(hello())

    负责启动异步系统,并执行最外层的异步函数。

    import asyncio

    async def main():
    print("程序开始")

    if __name__ == "__main__":
    asyncio.run(main())

    3.3 await

    await 的意思可以理解为:

    这个任务现在要等待,先把执行权让出去,等它完成以后再回来。

    import asyncio

    async def task():
    print("任务开始")

    await asyncio.sleep(2)

    print("任务结束")

    asyncio.run(task())

    await asyncio.sleep(2)不是单纯傻等两秒,它会告诉事件循环等待两秒,这段时间可以运行其他协程。

    4. time.sleep() 和 asyncio.sleep() 的区别

    这是异步编程最重要的区别之一:

    time.sleep(2):阻塞等待:全体都有,暂停两秒

    asyncio.sleep():异步等待:当前任务暂停2s,其他任务先走

    5. 写了 async 不代表自动并发

    先看代码:

    import asyncio

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

    async def main():
    await task("任务1")
    await task("任务2")
    await task("任务3")

    asyncio.run(main())

    结果:

    任务1 开始
    任务1 结束
    任务2 开始
    任务2 结束
    任务3 开始
    任务3 结束
    虽然函数使用了 async,但它仍然是依次执行,总耗时还是大约 6 秒。

    async 只代表它是异步函数,不代表多个任务自动并发执行。

    6. 真正让多个异步任务并发执行

    使用:asyncio.gather()

    import asyncio
    import time

    async def task(name):
    print(f"{name} 开始")
    await asyncio.sleep(2)
    print(f"{name} 结束")
    return f"{name} 的结果"

    async def main():
    start = time.time()

    results = await asyncio.gather(
    task("任务1"),
    task("任务2"),
    task("任务3"),
    )

    print(results)
    print(f"总耗时:{time.time() start:.2f} 秒")

    asyncio.run(main())

    结果:

    任务1 开始
    任务2 开始
    任务3 开始
    任务1 结束
    任务2 结束
    任务3 结束
    ['任务1 的结果', '任务2 的结果', '任务3 的结果']
    总耗时:2.01 秒

    三个任务都是等到2秒。

    await 会让当前函数暂停,等任务完成后才往下走,

    gather会同时启动 三个任务。

    7. 事件循环是什么

    异步程序背后有一个重要角色:Event Loop,事件循环.

    可以把事件循环想象成一个调度员。

    它不断检查:

    • 任务1能继续运行了吗?
    • 任务2的网络请求回来了吗?
    • 任务3的等待结束了吗?
    • 任务4的数据库结果返回了吗?

    大概流程:

    事件循环

    执行任务1

    任务1遇到 await,需要等待

    切换到任务2

    任务2遇到 await,需要等待

    切换到任务3

    任务1等待结束

    继续执行任务1

    协程的切换通常发生在 await 的位置,如果一个异步函数中完全没有 await,它运行时仍然可能一直占着事件循环导致代码卡死。

    8. 协程、任务、事件循环的关系

    协程 Coroutine就是一个可以暂停和恢复的函数。

    任务 Task是被事件循环安排执行的协程。

    可以理解成:

    • 协程:一份待执行的工作

    • Task:已经登记到事件循环里的工作

    事件循环 Event Loop负责调度这些 Task。

    协程函数
    ↓ 调用
    协程对象
    ↓ create_task
    Task

    事件循环调度执行

    9. asyncio.create_task() 的作用

    import asyncio

    async def task(name, seconds):
    print(f"{name} 开始")
    await asyncio.sleep(seconds)
    print(f"{name} 完成")
    return name

    async def main():
    task1 = asyncio.create_task(task("任务1", 3))
    task2 = asyncio.create_task(task("任务2", 2))
    task3 = asyncio.create_task(task("任务3", 1))

    print("三个任务已经启动")

    result1 = await task1
    result2 = await task2
    result3 = await task3

    print(result1, result2, result3)

    asyncio.run(main())

    结果:

    三个任务已经启动
    任务1 开始
    任务2 开始
    任务3 开始
    任务3 完成
    任务2 完成
    任务1 完成
    任务1 任务2 任务3

    关键代码:

    task1 = asyncio.create_task(…)

    意思是:把协程注册为任务,让事件循环尽快调度它,而不是立即等待它完成。

    10. 直接 await 和 create_task() 的区别

    直接 await:

    await task1()
    await task2()

    先等待task1完成
    再执行task2

    create_task:

    t1 = asyncio.create_task(task1())
    t2 = asyncio.create_task(task2())

    await t1
    await t2

    先启动task1
    再启动task2
    然后等待它们完成
    所以他们可以并发执行

    11. gather() 和 create_task() 怎么选

    只想同时执行多个任务

    使用:

    results = await asyncio.gather(
    task1(),
    task2(),
    task3(),
    )

    想先启动,后面再处理

    使用:

    task1 = asyncio.create_task(do_something())
    task2 = asyncio.create_task(do_something_else())

    print("这里可以继续做其他事情")

    result1 = await task1
    result2 = await task2

    12. 异步任务的返回值

    异步函数和普通函数一样,可以使用 return

    import asyncio

    async def calculate():
    await asyncio.sleep(1)
    return 100

    async def main():
    result = await calculate()
    print(result)

    asyncio.run(main())

    多个返回值:

    import asyncio

    async def get_user(user_id):
    await asyncio.sleep(1)
    return {
    "id": user_id,
    "name": f"用户{user_id}"
    }

    async def main():
    results = await asyncio.gather(
    get_user(1),
    get_user(2),
    get_user(3),
    )

    print(results)

    asyncio.run(main())

    13. 异步异常处理

    异步函数同样使用 try…except

    import asyncio

    async def task():
    await asyncio.sleep(1)
    raise ValueError("任务出错了")

    async def main():
    try:
    await task()
    except ValueError as e:
    print(f"捕获异常:{e}")

    asyncio.run(main())

    gather() 中有一个任务报错

    import asyncio

    async def task1():
    await asyncio.sleep(1)
    return "任务1成功"

    async def task2():
    await asyncio.sleep(1)
    raise ValueError("任务2失败")

    async def main():
    try:
    results = await asyncio.gather(
    task1(),
    task2(),
    )
    print(results)
    except Exception as e:
    print(f"出现异常:{e}")

    asyncio.run(main())

    这时异常不会直接让 gather() 中断,而是作为结果返回。

    14. 设置超时时间

    假设一个网络请求长时间没返回,不应该永远等下去。

    可以使用:asyncio.wait_for()

    import asyncio

    async def request_data():
    await asyncio.sleep(5)
    return "请求成功"

    async def main():
    try:
    result = await asyncio.wait_for(
    request_data(),
    timeout=2,
    )
    print(result)
    except asyncio.TimeoutError:
    print("请求超时")

    asyncio.run(main())

    因为任务需要 5 秒,而超时时间只有 2 秒,所以输出:请求超时

    15. 取消异步任务

    Task 可以取消:

    import asyncio

    async def task():
    try:
    print("任务开始")
    await asyncio.sleep(10)
    print("任务完成")
    except asyncio.CancelledError:
    print("任务被取消")
    raise

    async def main():
    t = asyncio.create_task(task())

    await asyncio.sleep(2)

    t.cancel()

    try:
    await t
    except asyncio.CancelledError:
    print("主程序确认任务已经取消")

    asyncio.run(main())

    16. 限制并发数量

    假设有 1000 个下载任务:await asyncio.gather(*tasks)

    可能会一瞬间发出 1000 个请求,可能导致:

    • 服务器拒绝连接
    • 本机连接数过多
    • API 限流
    • 内存占用过高
    • 数据库连接池被打满

    这时可以使用:asyncio.Semaphore,它叫信号量,可以限制同时运行的任务数量。

    import asyncio

    semaphore = asyncio.Semaphore(3)

    async def download(index):
    async with semaphore:
    print(f"任务 {index} 开始")

    await asyncio.sleep(2)

    print(f"任务 {index} 完成")

    async def main():
    tasks = [
    download(i)
    for i in range(10)
    ]

    await asyncio.gather(*tasks)

    asyncio.run(main())

    虽然有 10 个任务,但同一时刻最多只有 3 个任务进入

    17. 生产者和消费者:asyncio.Queue

    异步队列适合处理流水线任务:

    • 生产者:不断产生下载地址

    • 消费者:从队列中取地址并下载

    import asyncio

    async def producer(queue):
    for i in range(5):
    item = f"任务{i}"

    await queue.put(item)

    print(f"生产:{item}")

    await asyncio.sleep(0.5)

    await queue.put(None)

    async def consumer(queue):
    while True:
    item = await queue.get()

    if item is None:
    break

    print(f"消费:{item}")

    await asyncio.sleep(1)

    queue.task_done()

    async def main():
    queue = asyncio.Queue()

    await asyncio.gather(
    producer(queue),
    consumer(queue),
    )

    asyncio.run(main())

    队列为空时:消费者会暂停,但不会堵住整个程序。

    18. 异步适合处理的任务

    • 异步适合 I/O 密集型任务
    • 不适合 CPU 密集型任务

    19. 普通阻塞函数怎么放进异步程序

    有些老代码是同步阻塞函数,例如:

    import time

    def blocking_task():
    time.sleep(5)
    return "完成"

    直接在异步函数里调用:会阻塞事件循环。

    现代 Python 可以用:asyncio.to_thread()

    它会把普通阻塞函数放到线程中执行,避免堵住事件循环。

    import asyncio
    import time

    def read_file(filename):
    time.sleep(2)
    return f"读取完成:{filename}"

    async def main():
    result = await asyncio.to_thread(
    read_file,
    "data.txt",
    )

    print(result)

    asyncio.run(main())

    待续。。。。

    赞(0)
    未经允许不得转载:171主机测评 » Python异步编程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址