前言

你的Python程序还在"站着等饭"吗?当网络请求、文件读写、数据库查询卡住时,CPU只能空转发呆。本文从同步代码的8秒困境讲起,带你走进asyncio的世界,用协程和事件循环让单线程也能并发处理多件事,性能提升立竿见影。

一、为什么需要异步编程

1.1、问题场景描述

在现在Python开发、程序在执行过程中,经常需要等待一些操作完成。这些操作包括:等待网络请求返回数据、等待磁盘读写完成、等待数据库查询结果。这些操作称为I/O操作。

在传统的同步编程中,程序遇到等待时,会停下来干等。CPU在这段时间里什么也不做,资源就浪费了。这就像你去餐厅点餐,点完餐后站在厨房门口等着,别的什么事也不做。

1.2、场景代码演示

下面这段代码展示了同步编程的问题:

import time

def task1():
    # time.sleep模拟一个耗时5秒的I/O操作
    time.sleep(5)
    return 10

def task2():
    # 模拟一个耗时3秒的I/O操作
    time.sleep(3)
    return 20

def main():
    result = task1()
    print('任务1执行结果:', result)
    result = task2()
    print('任务2执行结果:', result)

if __name__ == '__main__':
    start = time.time()
    main()
    print('总耗时:', time.time() - start)
    

运行结果如下:

任务1执行结果: 10
任务2执行结果: 20
总耗时: 8.001485109329224

代码执行过程:

  • if __name__ == '__main__':首先执行这段代码,运行到start = time.time()这行代码时开始计时,接着进入到main()函数中,result = task1()调用执行task1函数,task1执行时,程序等待5秒,返回进入到main()函数
  • result = task2()调用执行task2函数,task2必须等待task1完成后才能开始,又等待3秒
  • 两个任务总共花了8秒
  • CPU在这8秒里大部分时间都在空等

理想情况应该是:

task1在等待时,CPU可以去执行task2。这样两个任务可以同时进行,总时间应该接近5秒(取两个任务中较长的那个)。

二、解决方法:异步编程

2.1、异步编程思想

异步编程的核心思想是:程序遇到等待时,不傻等,而是去做别的事情。等原来的那个操作完成了,再回来继续处理。

要实现这个效果,需要一套调度机制来管理任务。Python提供了asyncio模块,它的核心是事件循环。

事件循环的工作流程:

  1. 创建一个事件循环
  2. 把需要执行的任务注册到事件循环里
  3. 启动事件循环,开始调度任务

使用异步编程需要遵守两个规则:

  • 任务函数要用async def定义,表示这是异步函数
  • 函数内部遇到需要等待的地方,要用await标记

2.2、异步编程代码编写

import asyncio
import time

async def task1():
    print('task1 开始执行')
    # 注意:这里不能用time.sleep,因为time.sleep不是异步函数
    # 必须用asyncio.sleep,它是异步版本的sleep
    await asyncio.sleep(5)
    print('task1 结束执行')
    return 10

async def task2():
    print('task2 开始执行')
    await asyncio.sleep(3)
    print('task2 结束执行')
    return 20

async def main():
    print('main 开始执行')
    
    # 获取当前正在运行的事件循环
    event_loop = asyncio.get_running_loop()
    
    # 手动创建任务,注册到事件循环
    t1 = event_loop.create_task(task1())
    t2 = event_loop.create_task(task2())
    
    # 等待任务1完成,获取结果
    result = await t1
    print('任务1执行结果:', result)
    
    # 等待任务2完成,获取结果
    result = await t2
    print('任务2执行结果:', result)
    
    print('main 结束执行')

if __name__ == '__main__':
    start = time.time()
    
    # 创建事件循环
    event_loop = asyncio.get_event_loop()
    
    # 启动事件循环,运行main函数
    event_loop.run_until_complete(main())
    
    print('总耗时:', time.time() - start)
    

运行结果:

main 开始执行
task1 开始执行
task2 开始执行
task2 结束执行
task1 结束执行
任务1执行结果: 10
任务2执行结果: 20
main 结束执行
总耗时: 4.998461723327637

结果分析:

  • 哪一行代码写了await关键字就表示告诉事件循环此行代码可以挂起,CPU可以去执行其他的代码
  • 两个任务同时开始执行
  • task2先完成(3秒),task1后完成(5秒)
  • 总耗时约5秒,比同步方式的8秒快了很多

2.3、代码优化

上面的代码写起来有点复杂,需要手动创建事件循环和任务。其实有更简洁的写法:

import asyncio
import time


async def task1():
    print('task1 开始执行')
    # 注意:这里不能用time.sleep,因为time.sleep不是异步函数
    # 必须用asyncio.sleep,它是异步版本的sleep
    await asyncio.sleep(5)
    print('task1 结束执行')
    return 10


async def task2():
    print('task2 开始执行')
    await asyncio.sleep(3)
    print('task2 结束执行')
    return 20


async def main():
    print('main 开始执行')

    # asyncio.gather可以同时运行多个任务
    # 它会等待所有任务完成,返回一个结果列表
    results = await asyncio.gather(task1(), task2())

    # 等待任务1完成,获取结果
    print('任务1执行结果:', results[0])

    # 等待任务2完成,获取结果
    print('任务2执行结果:', results[1])

    print('main 结束执行')


if __name__ == '__main__':
    start = time.time()

    # asyncio.run会自动创建事件循环并运行任务
    # 这是最推荐的写法
    asyncio.run(main())

    print('总耗时:', time.time() - start)
    

运行结果:

main 开始执行
task1 开始执行
task2 开始执行
task2 结束执行
task1 结束执行
任务1执行结果: 10
任务2执行结果: 20
main 结束执行
总耗时: 5.0182108879089355

代码优化点:

  • asyncio.gather()代替手动创建任务,代码更简洁
  • asyncio.run()代替手动创建事件循环,一行搞定
  • 这种方式是官方推荐的标准写法

三、async和await关键字详解

3.1 async关键字

async用来定义异步函数,也叫协程函数。在调用async函数时,不会立即执行,而是返回一个协程对象。

import asyncio


async def task():
    print('task')


def demo():
    # 调用async函数,不会立即执行,而是返回一个协程对象
    coro = task()

    # 打印类型,结果是<class 'coroutine'>
    # coroutine类型代表的是协程对象 
    # 这说明task函数是一个异步函数,返回的是协程对象
    print(type(coro))


if __name__ == '__main__':
    demo()

    

运行结果:

<class 'coroutine'>

关于这段代码,有以下几个重要点需要理解:

  • 协程函数的返回值不是直接的结果,而是一个 协程对象
  • 协程对象 是一个未开始执行的任务。要执行这个任务,必须通过事件循环来调度。
import asyncio

async def task():
    print('task')

def demo():
    coro = task()
    # 创建事件循环,并将 coro 协程对象注册到事件循环中,由事件循环调度运行
    asyncio.run(coro)

if __name__ == '__main__':
    demo()
    

为什么用协程而不是普通函数

普通函数一旦开始执行,就会一直运行到结束,中间不能暂停。协程可以暂停,等需要等待的操作完成后,再恢复执行,也就是说协程能够支持暂停和恢复执行,这就是异步编程的基础。

3.2 await关键字

在Python中,await关键字主要用在async def 定义的协程函数中,用于暂停协程的执行,直到异步操作完成,然后继续执行后续代码,表示“等待某个操作完成”。

import asyncio

async def sub_task():
    print('sub task')
    return 100

async def task():
    # await会暂停当前task的执行
    # 等待sub_task执行完毕
    result = await sub_task()
    
    # sub_task完成后,继续执行后面的代码
    print('result:', result)

def demo():
    coro = task()
    asyncio.run(coro)

if __name__ == '__main__':
    demo()
    

await的使用规则:

  • await后面只能跟协程对象或future对象
  • 如果跟了其中其他类型的对象,会报错:TypeError: xxx can’t be used in ‘await’ expression`

await协程对象时的行为:

当 await 后面是协程对象时,事件循环暂停当前协程执行,然后继续执行 await 后面的协程对象中的代码

import asyncio

async def task02():
    print('task02')
    return 200

async def sub_task():
    print('sub_task')
    return 100

async def task01():
    print('task01')
    # await后面是协程对象,事件循环会继续执行sub_task
    # 但不会切换到其他任务线
    result = await sub_task()
    return result

async def start():
    # asyncio.gather同时注册多个任务
    result = await asyncio.gather(task01(), task02())
    print(result)


if __name__ == '__main__':
    asyncio.run(start())
    
task01
sub_task
task02
[100, 200]

注意:这里task01先执行,然后执行sub_task,然后才执行task02。说明await协程对象时,事件循环不会切换到其他任务。

await future对象时的行为:

但是,当 await 后面的对象换成了 future 对象,则事件循环会挂起当前任务,并转到其他的任务去执行。我们也可以理解为,await future 时,就是事件循环切换任务的一个时机。

import asyncio

async def task02():
    print('task02')
    return 200

async def sub_task():
    print('sub_task')
    return 100

async def task01():
    print('task01')
    
    # asyncio.sleep内部会创建future对象
    # await future时,事件循环会挂起当前任务,切换到其他任务
    await asyncio.sleep(3)
    
    result = await sub_task()
    return result

async def start():
    result = await asyncio.gather(task01(), task02())
    print(result)


if __name__ == '__main__':
    asyncio.run(start())
    
task01
task02
sub_task
[100, 200]

关键区别:

  • await协程对象:事件循环继续执行该协程,不切换任务
  • await future对象:事件循环挂起当前任务,切换到其他任务执行

asyncio.sleep()内部就是创建了future对象,所以它能触发任务切换。

四、future

future对象是事件循环和异步任务之间的桥梁。

future的作用场景:

假设程序需要通过网络获取数据。事件循环执行到这个步骤时,需要等待网络响应。为了不阻塞,它会:

  1. 创建一个新线程去执行网络请求
  2. 创建一个future对象,让事件循环和新线程都持有它
  3. 事件循环挂起当前任务,去执行其他任务
  4. 新线程拿到数据后,通过future对象把结果传回来
  5. 事件循环发现future有结果了,恢复原来的任务

future的作用就是帮程序获取未来某个时刻的操作结果。

import asyncio
import time
from concurrent.futures import ThreadPoolExecutor


def thread_task(futrue):
    time.sleep(5)
    futrue.set_result(100)


async def sub_task():
    print('sub task 开始')

    # 创建 future 对象
    event_loop = asyncio.get_running_loop()
    future = event_loop.create_future()
    # 创建线程池对象
    executor = ThreadPoolExecutor()
    # 在其他线程执行任务
    event_loop.run_in_executor(executor, thread_task, future)
    # 挂起当前任务,事件循环调度其他任务执行
    result = await future

    print('sub task 结束')
    return result


async def task1():
    print('task1 开始')
    result = await sub_task()
    print('task1 结束')
    return result


async def task2():
    print('task2 开始')
    await asyncio.sleep(1)
    print('task2 结束')
    return 200


async def main():
    result = await asyncio.gather(task1(), task2())
    print(result)


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

运行结果:

task1 开始
sub task 开始
task2 开始
task2 结束
sub task 结束
task1 结束
[100, 200]

结果分析:

  • task1开始执行,遇到网络等待
  • 事件循环创建线程去处理网络请求,同时挂起task1
  • 事件循环切换到task2执行
  • task2很快完成(1秒)
  • 5秒后,网络请求完成,future有了结果
  • 事件循环恢复task1,继续执行

五、小结

异步编程是一种在单线程里实现并发执行的技术。它通过事件循环来调度任务,遇到需要等待的操作时,就把控制权交给其他任务,等结果回来后再继续处理。这样就避免了"干等着"的情况,大大提升了运行效率。与多线程不同,异步不依靠开启多个线程,而是通过任务切换来充分利用时间。
通俗地说:异步编程就是让单线程也能同时干多件事,不用傻等,高效且不阻塞。
当程序的主要瓶颈是文件读写、网络请求、数据库操作等I/O操作时,异步编程可以在等待期间处理其他任务,最大化资源利用率。相比多线程,异步更轻量,性能更好。

结语

异步思维是一种编程习惯的升级。当你学会在await处放手、在事件循环中调度,你就掌握了让程序"不等白等"的秘诀。这不仅是技术,更是一种资源利用的智慧。把async和await用熟,你的Python代码会跑得更快、更轻、更优雅。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐