当前位置: 首页 > 科技观察

如何让Python的处理速度翻倍?附带的代码

时间:2023-03-18 14:17:21 科技观察

是日常开发生产中非常实用的语言。需要掌握一些python的用法,比如爬虫、网络请求等场景,非常实用。但是python是单线程的。如何提高python的处理速度是一个很重要的问题。这个问题的一个关键技术叫做协程。这篇文章讲的是python协程的理解和使用,主要是对网络请求模块进行梳理,希望对有需要的同学有所帮助。概念篇在了解协程的概念及其功能场景之前,首先要了解操作系统的几个基本概念,主要是进程、线程、同步、异步、阻塞、非阻塞。理解这些概念不仅对协程的场景有帮助,比如消息队列、缓存等,接下来小编根据自己的理解结合网上查询的资料做一个总结。在采访进程的时候,我们都会记住一个概念,进程是系统资源分配的最小单位。是的,系统是由程序组成的,也就是进程。一般分为文本区、数据区和堆栈区。文本区存放处理器执行的代码(机器码)。一般来说,这是一个只读区域,以防止意外修改正在运行的程序。数据区存放所有变量和动态分配的内存,分为初始化数据区(所有初始化的全局变量、静态变量、常量变量和外部变量)和未初始化数据区(全局变量和静态变量初始化为0),初始化变量为最初保存在文本区,程序启动后复制到初始化数据区。堆栈区存储活动过程调用的指令和局部变量。在地址空间中,栈区紧挨着堆区。他们的成长方向是相反的。内存是线性的,所以我们的代码是放在低地址的,从低到高增长,栈区的大小是不可预知的,开着就可以用,所以放在高地址和从高到低增长。当堆和栈指针重合时,说明内存耗尽,造成内存溢出。进程的创建和销毁是相对于系统资源而言的,消耗资源较多,是比较昂贵的操作。为了让一个进程自己运行,它必须先发制人地竞争CPU。对于单核CPU来说,只能同时执行一个进程的代码,所以在单核CPU上实现多进程就是通过CPU在不同进程之间快速切换,看起来就像是有多个进程在运行一样同时。由于进程之间是隔离的,各自有自己的内存资源,相对于线程的共同共享内存,相对安全。不同进程之间的数据只能通过IPC(Inter-ProcessCommunication)进行通信和共享。线程线程是CPU调度的最小单位。如果进程是容器,那么线程就是运行在容器中的程序,线程属于进程,同一个进程的多个线程共享进程的内存地址空间。线程间通信可以直接通过全局变量进行通信,所以相对来说,线程间通信不是很安全,所以引入各种加锁场景,这里不再赘述。当一个线程挂掉的时候,会导致整个进程挂掉,也就是其他线程也会挂掉,但是多个进程不会,一个进程挂掉了,另一个进程还在运行。在多核操作系统中,默认进程中只有一个线程,所以多进程处理就像一个进程一个内核。同步与异步同步与异步关注的是消息通信机制。所谓同步,就是当一个函数调用发出后,直到得到结果,调用才会返回。一旦调用返回,立即获取执行的返回值,即调用者主动等待调用结果。所谓异步,就是请求发出后,调用立即返回,不返回结果,调用的实际结果通过回调等方式通知。同步请求需要主动读写数据,等待结果;异步请求,调用者不会立即得到结果。相反,在调用发出后,被调用者通过状态和通知通知调用者,或者通过回调函数处理调用。阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)时的状态。阻塞调用意味着当前线程将在调用结果返回之前被挂起。调用线程只有在得到结果后才会返回。非阻塞调用是指调用不会阻塞当前线程,直到不能立即得到结果。所以区分的条件是进程/线程要访问的数据是否准备好,进程/线程是否需要等待。非阻塞一般是通过多路复用来实现的,多路复用有几种实现方式:select、poll、epoll。了解了前面协程的概念之后,我们再来看看协程的概念。协程属于线程,又称微线程、纤程,英文名Coroutine。比如在执行函数A时,我想随时中断函数B的执行,然后中断B的执行,切换回来执行A。这就是协程的功能,由调用者自由切换.这个切换过程不等同于函数调用,因为它没有调用语句。执行方式与多线程类似,只是协程只有一个线程执行。协程的优点是执行效率非常高,因为协程的切换是由程序自己控制的,不需要切换线程,也就是没有切换线程的开销。同时,由于只有一个线程,不存在冲突问题,也不需要依赖锁(加锁和释放锁会消耗大量资源)。协程的主要使用场景是处理IO密集型程序,解决效率问题,但不适合CPU密集型程序的处理。但是,这两种场景在实际场景中有很多。如果想充分发挥CPU利用率,可以结合多进程+协程。稍后我们将讨论连接。原理根据维基百科的定义,协程是一种无优先级的子程序调度组件,可以让子程序在特殊的地方挂起和恢复。所以理论上只要内存足够,线程中可以有任意数量的协程,但一次只能运行一个协程,多个协程共享分配给线程的计算机资源。协程是为了充分发挥异步调用的优势,异步操作是为了避免IO操作阻塞线程。知识准备在了解原理之前,我们先做一个知识准备。1)现代主流操作系统几乎都是分时操作系统,即一台计算机采用时间片轮换的方式为多个用户服务。系统资源分配的基本单位是进程,CPU调度的基本单位是线程。2)运行时内存空间分为变量区、栈区、堆区。在内存地址分配上,堆区从低位到高位,栈区从高位到低位。3)计算机执行时,一条条读取指令并执行。当前指令执行时,下一条指令的地址在指令寄存器的IP中,ESP寄存器值指向当前栈顶地址,EBP指向当前活动栈帧的基地址。4)系统调用函数时,操作如下:先将输入参数从右向左压栈,再将返回地址压栈,最后将EBP寄存器的当前值压栈,修改ESP寄存器的值,分配当前函数局部变量所需的空间。5)协程的上下文包括属于当前协程的栈区和寄存器中存放的值。在python3.3中,事件循环通过关键字yieldfrom来使用协程。3.5引入了关于协程的语法糖async和await。我们主要看async/await的原理分析。其中,事件循环是一个核心。写过js的同学对事件循环会比较了解。事件循环是一种等待程序分发事件或消息的编程架构(维基百科)。在python中,asyncio.coroutine装饰器用于将函数标记为协程,其中协程与asyncio及其事件循环一起使用,在后续的开发中,async/await的使用越来越广泛。async/awaitasync/await是使用python协程的关键。从结构上看,asyncio本质上是一个异步框架。async/await是为异步框架提供的API,方便用户调用,所以用户想要使用async/await来编写协程代码,就必须要有asyncio或者其他异步库的访问权限。Future在实际开发编写异步代码的时候,为了避免回调方法过多造成的回调地狱,同时又需要获取异步调用的返回结果,聪明的语言设计者设计了一个名为Future的对象,封装了循环和循环交互行为。大致执行流程如下:程序启动后,通过add_done_callback方法向epoll注册回调函数。当result属性拿到返回值后,主动运行之前注册的回调函数向上传递给协程。Future对象是asyncio.Future。但是为了获取返回值,程序必须恢复工作状态,并且由于Future对象本身的生命周期比较短,每次注册回调,产生事件,工作可能已经完成,并且回调过程被触发,所以使用Future发送到生成器结果是不合适的。所以这里引入了一个新的对象Task,存放在Future对象中,用于管理生成器协程的状态。Python中的另一个Future对象是concurrent.futures.Future,它与asyncio.Future不兼容,容易混淆。不同的是,concurrent.futures是一个线程级的Future对象,当concurrent.futures.Executor用于多线程编程时,用于在不同线程之间传递结果。Task上面提到,Task是一个任务对象,它维护生成器协程的状态来处理执行逻辑。Task中有一个_step方法,负责生成器协程与EventLoop交互过程的状态转换。整个过程可以理解为:Task向协程发送一个值,恢复其工作状态。当协程运行到断点时,会得到一个新的Future对象,然后处理future和loop的回调注册过程。在循环的日常开发中,会有一个误区,认为每个线程都可以有一个独立的循环。实际运行时,主线程可以通过asyncio.get_event_loop()创建一个新的循环,但是在其他线程中,使用get_event_loop()会抛出错误。正确的做法是通过asyncio.set_event_loop()将当前线程显式绑定到主线程的循环中。Loop有一个很大的缺陷,就是循环的运行状态不受Python代码控制,所以在业务处理中,不可能稳定地扩展协程运行在多线程中。小结介绍完实战篇的概念和原理,下面就来看看如何使用吧。这里我举一个实际场景的例子,看看如何使用python的协程。有些文件是在场景外接收的,每个文件包含一组数据。其中,这组数据需要通过http发送给第三方平台,获取结果。分析由于同一个文件中的每组数据没有前后处理逻辑,通过Requests库发送的网络请求是串行执行的,下一组数据的发送需要等待上一组数据的返回一组数据,好像是对整个文件的处理很长一段时间,这种请求方式完全可以用协程来实现。为了配合协程更方便的发送请求,我们使用aiohttp库代替requests库。关于aiohttp,我们这里不做过多的分析,只是做一个简单的介绍。aiohttpaiohttp是用于asyncio和Python的异步HTTP客户端/服务器。因为是异步的,所以常用于服务区接收请求,客户端爬虫应用发起异步请求。这里我们主要用它来发送请求。aiohttp支持client和HTTPserver,可以实现单线程并发IO操作,可以在不使用CallbackHell的情况下支持ServerWebSockets和ClientWebSockets,并且有中间件。代码实现直接上代码了,话不多说,给我看代码~importaiohttpimportasynciofrominspectimportisfunctionimporttimeimportlogger@logging_utils.exception(logger)defrequest(pool,data_list):loop=asyncio.get_event_loop()loop.run_until_complete(exec(pool,data_list))asyncdefexec(pool,data_list):tasks=[]sem=asyncio.Semaphore(pool)foritemindata_list:tasks.append(control_sem(sem,item.get("method","GET"),item.get("url"),item.get("data"),item.get("headers"),item.get("callback")))awaitasyncio.wait(tasks)asyncdefcontrol_sem(sem,method,url,data,headers,callback):asyncwithsem:count=0flag=Falsewhilenotflagandcount<4:flag=awaitfetch(方法、url、数据、标题、回调)count=count+1print("flag:{},count:{}".format(flag,count))ifcount==4andnotflag:raiseException('EASservicenotrespondingafter4timesofretry.')asyncdeffetch(method,url,data,headers,callback):asyncwithaiohttp.request(method,url=url,data=data,headers=headers)asresp:try:json=awaitresp.read()print(json)ifresp.status!=200:returnFalseifisfunction(callback):callback(json)returnTrueexceptExceptionase:print(e)这里,我们封装了外部批量请求的request方法,一次接收多少数据,以及数据集成,对外使用时,只需要构建网络请求对象的数据,设置请求池的大小,同时设置重试功能,并进行4次重试,防止网络抖动时单条数据的网络请求失败。最终的效果是使用协程重构网络请求模块后,在数据量为1000时,从之前的816s增加到424s,速度更快。次,并且当请求池大小增加时,效果更明显。由于第三方平台同时建立连接的数据量有限,我们设置了40个阈值,可以看出优化程度非常显着。参考资料:理解async/await:https://segmentfault.com/a/1190000015488033?spm=ata.13261165.0.0.57d41b119Uyp8t协程概念、原理(c++和node.js实现)https://cnodejs.org/topic/58ddd7a303d476b42d34c911?spm=ata.13261165.0.0.57d41b119Uyp8t