Python 异步编程

高性能异步编程

一、 引入背景

1、 概述

其实爬虫的本质就是client发请求批量获取server的响应数据,如果我们有多个url待爬取,只用一个线程且采用串行的方式执行,那只能等待爬取一个结束后才能继续下一个,效率会非常低。需要强调的是:对于单线程下串行N个任务,并不完全等同于低效,如果这N个任务都是纯计算的任务,那么该线程对cpu的利用率仍然会很高,之所以单线程下串行多个爬虫任务低效,是因为爬虫任务是明显的IO密集型(阻塞)程序。那么该如何提高爬取性能呢?

2、 分析处理

同步调用:即提交一个任务后就在原地等待任务结束,等到拿到任务的结果后再继续下一行代码,效率低下

import requests

def parse_page(res):
    print('解析 %s' %(len(res)))

def get_page(url):
    print('下载 %s' %url)
    response=requests.get(url)
    if response.status_code == 200:
        return response.text

urls = ['https://www.baidu.com/','http://www.sina.com.cn/','https://www.python.org']
for url in urls:
    res=get_page(url)  # 调用一个任务,就在原地等待任务结束拿到结果后才继续往后执行
    parse_page(res)

解决方案

  1. 多线程 / 多进程
    • 优点:在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接
    • 弊端:开启多进程或都线程的方式,我们是无法无限制地开启多进程或多线程的:在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态
  2. 线程/进程池
    • 好处:很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。可以很好的降低系统开销
    • 弊端:“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小

二、 终极处理方案

  • 上述无论哪种解决方案其实没有解决一个性能相关的问题:IO阻塞,无论是多进程还是多线程,在遇到IO阻塞时都会被操作系统强行剥夺走CPU的执行权限,程序的执行效率因此就降低了下来

  • 解决这一问题的关键在于,我们自己从应用程序级别检测IO阻塞然后切换到我们自己程序的其他任务执行,这样把我们程序的IO降到最低,我们的程序处于就绪态就会增多,以此来迷惑操作系统,操作系统便以为我们的程序是IO比较少的程序,从而会尽可能多的分配CPU给我们,这样也就达到了提升程序执行效率的目的。

  • 在python3.4之后新增了asyncio模块,可以帮我们检测IO(只能是网络IO【HTTP连接就是网络IO操作】),实现应用程序级别的切换(异步IO)。注意:asyncio只能发tcp级别的请求,不能发http协议。

  • 异步IO:所谓「异步 IO」,就是你发起一个 网络IO 操作,却不用等它结束,你可以继续做其他事情,当它结束时,你会得到通知

    • 实现方式:单线程+协程实现异步IO操作

三、 异步协程

1、 协程

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。

协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。

我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势

2、 用法

接下来让我们来了解下协程的实现,从 Python 3.4 开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础的,在 Python 3.5 则增加了 async/await,使得协程的实现更加方便

Python 中使用协程最常用的库莫过于 asyncio,所以本文会以 asyncio 为基础来介绍协程的使用

首先我们需要了解下面几个概念:

  • [1] event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法
  • [2] coroutine:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象
  • [3] task:任务,它是对协程对象的进一步封装,包含了任务的各个状态
  • [4] future:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。 另外我们还需要了解 async/await 关键字,它是从 Python 3.5 才出现的,专门用于定义协程。其中,async 定义一个协程,await 用来挂起阻塞方法的执行

3、 实现协程

3.1 greenlet
pip insatll greenlet
from greenlet import greenlet

def func1():
    print(1)  # 第一步
    gr2.switch()  # 第二步切换到 func2 函数
    print(2)  # 第六步
    gr2.switch()  # 第七步切换到 func2,从上次执行位置执行

def func2():
    print(3)  # 第三步
    gr1.switch()  # 第五步,切换到 func1 上次执行的位置
    print(4)

gr1 = greenlet(func1)
gr2 = greenlet(func2)
gr1.switch()  # 执行 func1
3.2 yield
def func1():
    yield 1
    yield from func2()
    yield 2

def func2():
    yield 3
    yield 4

f1 = func1()
for item in f1:
    print(item)
3.3 asyncio

在 python3.5 之后的版本支持

import asyncio

@asyncio.coroutine
def func1():
    print(1)
    # 网络 IO 请求
    yield from asyncio.sleep(2)  # 遇到 IO 耗时操作,自动切换
    print(2)
    
@asyncio.coroutine
def func2():
    print(3)
    # 网络 IO 请求
    yield from asyncio.sleep(2)  # 遇到 IO 耗时操作,自动切换
    print(4)
    
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

遇到 IO 阻塞自动切换

3.4 async & await
import asyncio

async def func1():
    print(1)
    # 网络 IO 请求
    await asyncio.sleep(2)  # 遇到 IO 耗时操作,自动切换
    print(2)

async def func2():
    print(3)
    # 网络 IO 请求
    await asyncio.sleep(2)  # 遇到 IO 耗时操作,自动切换
    print(4)

tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2())
]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

4、 协程的意义

在一个线程中如果遇到 IO 等待时间,线程不会等待,而是利用空闲时间再去干点其他事情

案例:

  • 普通方式:同步方式

    下载三张图片

    from requests import get
    from time import time
    
    
    def download_img(url):
        name = url.spl
  • 10
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
Python 异步编程是一种编程范式,它利用并发来提高程序的执行效率,特别是在处理I/O密集型任务时,比如网络请求、文件操作等。异步编程的核心在于避免了线程或进程切换带来的开销,让程序能够更高效地处理多个任务。 在Python中,异步编程主要通过以下几个库来实现: 1. **asyncio**:这是Python标准库的一部分,提供了创建异步任务和协程的基础。通过`async`和`await`关键字,可以编写协程(coroutine),这些是可以在事件循环中运行的轻量级代码块。 2. **Future 和 Task**:`asyncio.Future`和`asyncio.Task`用于封装异步操作的结果,Task是Future的包装器,提供了一些额外的功能,如跟踪状态和取消操作。 3. **Coroutines**(协程):通过定义带有`async def`的函数,函数内部可以使用`await`来挂起执行,直到依赖的异步操作完成。 4. **AIO库**(如Aiohttp、aioredis等):这些第三方库针对特定场景提供了异步版本,如Aiohttp用于非阻塞的HTTP客户端,aioredis用于异步操作Redis数据库。 5. **异步装饰器**:如`@aio.coroutine`(在Python 3.5及更早版本中使用)或`async def`(在Python 3.6及以上版本中)等,可以将常规函数转换为异步协程。 异步编程的一些关键概念包括: - **事件循环**:协调和调度所有协程的运行。 - **异步I/O**:通过非阻塞I/O,允许程序在等待I/O操作完成时继续执行其他任务。 - **回调和生成器**:早期的异步编程可能使用这些技术,但现代Python更倾向于使用async/await和Task。 如果你对异步编程有深入的兴趣,可能会问到: 1. 异步编程如何提高程序性能? 2. Python中如何正确地管理异步任务的执行顺序? 3. 异步编程中的“回调地狱”是什么,如何避免?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

SteveKenny

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

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

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

打赏作者

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

抵扣说明:

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

余额充值