Python异步编程实战入门:从概念到实战

本文介绍了Python异步编程的概念,包括异步的基本模型、原理和实现机制。通过实例展示了如何使用aiohttp和aiofiles库进行异步网络请求和文件读取,以及如何利用异步提高爬虫性能。此外,还探讨了多线程与异步的结合,以实现更高效的程序运行。
摘要由CSDN通过智能技术生成

概述

读者可前往我的博客获得更好的阅读体验

在Python中存在GIL机制,该机制保证了在Python中同时间内仅能运行一行代码,这导致了Python无法真正实现多线程,但可以通过多进程打破GIL限制,我们会在本文的最后讨论此内容。但Python中存在另一种神奇的机制,即异步机制。在计算机领域,我们经常提到异步、并行、多线程等名词,但本文不想讨论这些名词具体的含义,这些对于概念的讨论在很多情况下是无意义的。本文将专注于介绍异步机制,在本文的最后,我们会引入多线程等内容以进一步提高Python性能。

本文主要讨论以下内容:

  1. 异步的概念与Hello World
  2. 异步编程的基本模型和关键词
  3. 异步的实现机制
  4. 完整的异步爬虫示例
  5. 增加多进程支持的性能更强的异步爬虫

由于异步属于Python中较为先进且不断变化的机制,笔者使用了3.11作为基准版本,本文中的大部分代码可能无法在3.11以下版本运行,笔者会尽可能将低版本兼容方案列出。本文也将使用以下库:

  • aiohttp
  • aiofiles

请读者自行使用pip进行安装。如果您遇到error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/报错,请参考我在CSDN写的如何在Python中简单地解决Microsoft Visual C++ 14.0报错一文。

异步的概念与Hello World

异步是指在程序运行过程中,一些等待操作(如IO操作)不会阻塞代码的运行。该概念较为抽象,我们给出一个非异步的代码示例:

import time


def count():
    print("One")
    time.sleep(1)
    print("Two")


def main():
    for _ in range(3):
        count()


if __name__ == "__main__":
    s = time.perf_counter()
    main()
    elapsed = time.perf_counter() - s
    print(f"Code runtime: {
     elapsed:.2f}")

上述代码较为简单,运行结果如下:

One
Two
One
Two
One
Two
Code runtime: 3.01

当主函数运行到count()时,函数首先输出One,然后因为time.sleep会暂停 1 秒,然后继续输出Two。我们可以看到time.sleep使整个代码进入的停顿情况,在这sleep的 1 秒内,所有的运行都被停止。接下来,我们给出一个异步版本:

import asyncio


async def count():
    print("One")
    await asyncio.sleep(1)
    print("Two")


async def main():
    await asyncio.gather(count(), count(), count())

if __name__ == "__main__":
    import time
    s = time.perf_counter()
    asyncio.run(main())
    elapsed = time.perf_counter() - s
    print(f"Code runtime: {
     elapsed:.2f}")

在此处,我们引入了一些不太常见的函数:

  1. asyncio.sleep 异步计时器
  2. asyncio.gather 此函数用于并发执行一系列函数,这里的并发并不意味着后续三个count()函数一起运行,具体原理会在下文讨论
  3. asyncio.run 此函数用于启动异步任务main()
  4. await 等待调用

根据文档相关表述,一个更加现代但仅适用3.11的代码如下:

async def main():
    async with asyncio.TaskGroup() as tg:
        for i in range(3):
            tg.create_task(count())

上述代码运行结果如下:

One
One
One
Two
Two
Two
Code runtime: 1.01

显然,此代码运行速度更快,且输出结果也与同步版本不符,其运行流程图如下:

Asynio Example

asynio.gather函数运行后,第一个count()函数启动,运行并输出One,但其运行到await asyncio.sleep(1)会运行asyncio.sleep暂停运行,将控制权交还给主函数,主函数继续运行下一个count()函数,重复上一流程。当第一个count()函数暂停满 1 秒后,它会取得运行权限,将Two进行输出,其他count()函数类似。

我们可以发现await事实上的含义为运行函数等待返回,并等待返回数据过程中交出控制权使其他函数运行。

在现实的编码实例中,我们可以考虑asyncio.sleep(1)为一耗时的系统操作,这一操作不需要Python代码运行而仅需要Python等待运行结果返回,比如网络请求。在网络请求过程中,发送数据包和接收数据包都较为快速,对于爬虫而言,大部分时间浪费在等待数据返回的过程中,这一等待过程中Python不需要操作。所以我们可以引入异步机制让Python在此等待时间内进行其他工作。另一个比较常见的案例即读取文件,读取文件的过程并不是Python完成的,Python只是在读取文件时向操作系统发出请求,等待操作系统返回文件内容,这一过程也是浪费的时间,可以通过异步方法使Python进行其他工作。

IO waitting

值得注意的是,Python的原生实现,包括requests等网络请求库均没有实现上述异步特性,为实现异步,我们需要引入概述中给出的两个库,其中:

  • aiohttp 实现网络请求异步
  • aiofiles 实现文件读取异步

基本模型

本节主要介绍一些在异步编程中常用的编程模型。

第一种就是最简单的链式异步调用,我们以一个简单的例子进行介绍。目前存在一个简单的API进行GET请求后会返回用户信息列表,我们需要请求此API并print结果。首先,我们给出基于requests的同步版本:

import requests
import time
from requests import session

def get_api(s: session):
    url = "https://mocki.io/v1/d4867d8b-b5d5-4a48-a4ab-79131b5809b8"
    req = s.get(url).json()
    print(req)


def main():
    s = session()
    for i in range(3):
        get_api(s)


if __name__ == "__main__":
    s = time.perf_counter()
    main()
    elapsed = time.perf_counter
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WongSSH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值