Python进阶:聊聊IO密集型任务、计算密集型任务,以及多线程、多进程

Python中常见的并发机制有:多线程和多进程。
多线程适合IO密集型任务,而多处理适合计算密集型任务。
在Python中,多线程读取是通过在一个进程中启动多个线程来完成的。
然而,由于全局解释器锁(GIL)的存在,Python的多线程是“互锁执行”而不是真正的并行。
因此,对于计算密集型任务,多线程并不适合。
相比之下,多处理可以充分利用CPU资源,特别是对于计算密集型任务。
Python提供了multiprocessing模块等多处理接口,支持创建进程、传输数据等。
进程之间的交互是通过管道或队列完成的。
为了直观地说明多线程、多进程的执行场景,以IO密集型任务为例。
首先,定义队列和初始化队列的函数。
然后依次执行IO密集型任务和计算密集型任务,并从队列中取出任务信息。
通过比较不同相关方法的执行时间,我们发现多线程适合IO密集型任务,而多线程更适合计算机密集型任务。
通过使用示例代码验证实际操作,并比较多线程和多进程执行同一任务所需的时间,证明多进程可以显着提高基于计算机的任务的效率。
代码示例和详细测试结果上传至GitHub,欢迎访问:xianhu/LearnPython。
如果您对Python多线程和多处理有任何疑问或建议,请随时加入GitHub页面的讨论。
让我们和睦相处,共同学习,共同成长。

python多进程为什么一定要

我们讨论了为什么在Python中建议使用多进程而不是多线程,但是多进程也有其自身的局限性。
比线程更麻烦,切换时间更长,而且在Python的多处理中,建议进程数不要超过CPU。
核心数(一个进程只有一个GIL,因此一个进程只能运行在一个CPU上)。
这是因为,如果一个进程占用一个CPU,那么机器的性能就可以充分发挥出来。
但是,如果进程太多,则收益不值得,因为会频繁切换进程。
然而,在特殊情况下(尤其是IO密集型任务),多线程比多处理更容易使用。
例如,如果给出200万个URL,则必须捕获并存储每个URL对应的页面,这样的话,单独使用多个进程的效率显然很低。
为什么?例如,如果每个请求的延迟为2秒(忽略CPU计算时间):1、单进程+单线程:耗时2秒*200W=400W秒==1111.11小时==46.3天这个速度显然是让人无法接受的。
2、单进程+多线程:比如在这个进程中开启10个多线程,可以比1个快10倍。
完成200万个项目的爬网大约需要4.63天。
这里实际执行的是线程1发现块,CPU切换到线程2执行,发现块并切换到线程3,以此类推。
当所有10个线程都被阻塞后,进程就会被阻塞,直到该特定线程被阻塞之前,没有进程可以继续运行,这可以将速度提高大约10倍(这里忽略线程切换带来的开销)。
成本,实际提升应该不会达到10x)但是要考虑的是线程切换也是有开销的,所以不能无限启动多线程(开200万个线程肯定不靠谱)3.多进程+多线程:这个太棒了。
通常很多人都有多进程,每个进程可以占用一个CPU,多线程在一定程度上绕过了等待。
比如你开启10个进程,每个进程开启20W线程,理论上运行速度会比单个进程运行200W线程快10倍以上。
不是10倍,而是10倍以上吗?主要原因是切换200W的线程的CPU消耗显然比切换20W的进程要多得多。
考虑到这个成本,要高出10倍以上。
有更好的办法吗?答案是肯定的。
4.在使用协程之前,我们先来谈谈what/why/how(它们是什么/为什么我们使用它们/我们如何使用它们)。
内容:协程是轻量级的用户级线程。

协程有自己的寄存器上下文和堆栈。
当协程调度切换时,寄存器上下文和堆栈存储在不同的位置。
切换回来会恢复之前保存的寄存器上下文和堆栈。
因此,协程可以维护最后的调用状态(即所有本地状态的特定组合)。
进程每次重新进入,就相当于进入了最后一次调用的状态。
我是最后一个离开的。
逻辑流程地点。
在并发编程中,协程类似于线程。
每个协程代表一个执行单元,拥有自己的本地数据,并与其他协程共享全局数据和其他资源。
原因:当今主流语言默认选择多线程作为其并发功能。
与线程相关的概念是抢占式多任务,与协程相关的概念是协作多任务。
每当有东西阻塞或切换时,无论是进程还是线程,它首先会导致CPU运行操作系统的调度程序,然后调度程序运行某个进程(线程)。
而且,由于抢占式调度中的执行顺序是不可预测的,因此在使用线程时必须非常小心地处理同步问题,而协程则没有这样的问题(事件驱动和异步程序具有相同的优点)。
因为协程的调度逻辑是由用户编写的,所以对于CPU来说协程实际上是单线程的,因此CPU不必考虑如何调度或切换上下文。
这样就节省了CPU切换开销,所以协程是:在某些情况下,它甚至比多线程还要大。
Howto:如何在Python中使用协程?答案是使用gevent。
具体方法:下面看一下不受线程开销限制的协程的使用。
我曾经尝试在单个进程协程中运行200,000个URL,并且完全没有问题。
因此,最推荐的做法是多进程+协程(每个进程可以认为是一个单线程,这个单线程就是一个协程)。
使用多进程+协程避免了CPU切换的开销。
该方法可以显着提高爬虫执行大数据和文件读写操作的效率。
小例子:#-*-coding=utf-8-*-importrequestsfrommultiprocessingimportProcessimportgeventfromgeventimportmonkey;monkey.patch_all()importtsysreload(sys)sys.setdefaultencoding('utf8')deffetch(url):try:s=requests.Session()r=s.get(url,timeout=1)#此处捕获页面exceptExceptionion,e:printereturn''defprocess_start(tasks):gevent.joinall(tasks)#使用协程执行deftask_start(filepath,flag=100000):#每100W个URL启动witho进程pen(filepath,'r')asreader:#从给定文件中读取URLurl=reader.readline().strip()Task_list=[]#This列表用于存储协程任务。
i=0#Counter,记录添加到协程队列中的几个URL。
whileurl!='':i+=1​task_list.append(gevent.spawn(fetch,url,queue))#每次读取url时向协程队列添加一个任务。
ifi==flag:#一定数量的URL将启动该进程并运行它。
p=process(target=process_start,args=(tAsk_list,))p.start()Task_list=[]#重置协程队列i=0#重置计数器url=reader.readline().strip()iftask_listnot[]:#如果终止后任务队列中还有任务,则循环中还剩下URLp=Process(target=pprocess_start,args=(task_list,))#Remainder将最后一个进程中的所有URL放入,运行p.start()【if__name__=='__main__':task_start('./testData.txt')#请仔细阅读指定文件学生可以看到上面有隐藏项有的。
有一个隐藏的问题。
随着URL数量的增加,进程数量不断增加。
这里可以使用进程池来控制进程的数量。
不使用multiprocessing.Pool的原因是因为multiprocessing.Pool与gevent冲突。
虽然不能同时使用,但是同学们可以研究协程池gevent.pool。
还有一个问题。
每个进程处理的URL不是独立的;它们是累积的。
例如,第一个进程处理100,000个URL,第二个进程处理200,000个URL。
最后确定问题是gevent.joinall()引起的。
有兴趣的同学可以研究一下为什么会出现这种情况。
不过,这个问题的解决方案是主进程只负责读取URL并将其写入列表。
创建子进程时,列表直接传递给子进程,子进程自己构建协程。

这将避免任何堆积问题。

Python线程,进程,多线程,多进程以及并行执行for循环笔记

Python中的并行处理工具如线程、进程、多线程、多进程以及for循环的并行执行有助于提高程序效率。
首先,Python中的线程和进程用于解决不同类型的问题:线程适用于网络请求等I/O密集型任务,多处理适用于繁重计算等CPU密集型任务。
GIL(全局解释器锁)限制了Python中多线程的并行性,但多进程可以利用多核处理器。
Python的多处理模块提供了强大的多处理功能,适合CPU密集型任务。
Pool类及其close()和join()方法用于管理进程池并确保任务完成后正确终止。
使用multiprocessing.map时,需要根据任务特性和资源共享情况来决定是否添加锁,以避免数据争用。
在Python中,虽然for循环本身不能直接同时执行多个线程,但是可以通过threads模块创建子线程来实现。
值得注意的是,GIL会影响CPU密集型任务的并行性。
concurrent.futures模块提供了ThreadPoolExecutor和ProcessPoolExecutor等高级接口来简化并行任务的管理。
joblib库适用于科学计算和机器学习,并提供内存缓存功能以减少重复计算。
在concurrent.futures模块中,ThreadPoolExecutor和ProcessPoolExecutor分别用于线程池和进程池。
它们提供可调用对象的调度和未来对象的管理。
as_completed函数允许您按照任务完成的顺序获取结果,而无需等待所有任务完成。

python多进程和多线程的区别

进程是程序(程序、应用程序)的执行实例,每个运行的程序可以同时创建多个进程,但必须至少有一个进程。
每个进程提供执行程序所需的所有资源,并包含虚拟地址空间、可执行代码、操作系统接口、安全上下文(启动进程的用户注册和权限等)以及唯一的进程标识符。
环境变量、优先级、最小和最大工作空间(内存空间)。
进程可以有线程,并且每个进程必须至少有一个线程。
每个进程启动时,都会先创建一个线程,即主线程,然后主线程再创建其他子线程。

线程,有时也称为轻量级进程(LightweightProcess,LWP),是程序执行流程的最小单元。
标准线程由线程标识符、当前指令指针(PC)、寄存器组和堆栈组成。
另外,线程是进程中的一个实体,是系统独立调度和分派的基本单位。
线程本身并不独立拥有系统资源,但可以与属于同一进程的其他线程共享该进程拥有的所有资源。
每个应用程序至少有一个进程和一个线程。
在单个程序中同时运行多个线程来完成分成多个部分的不同任务称为多线程。

举个例子,一家公司要生产一种产品,所以在自己的生产基地建设了几个工厂,每个工厂都有多条生产线。
所有工厂合作生产整个产品,一个工厂的装配线负责生产所属工厂产品的零部件,每个工厂都有自己的物料库,工厂内的生产线共享。
这些材料。
一个企业要实现生产,必须至少拥有一个工厂和一条生产线。
转向计算机的概念,公司就是应用程序,工厂就是应用程序流程,生产线就是特定流程的线程。

线程的特点:

线程是一个执行上下文,是CPU执行所需的一系列指令。
假设您正在读一本书,您还没有读完,您想休息一下,但您想在回来时继续原来的内容。
一种方法是写出页数、行数、字数这三个值,这些值就是执行上下文。
如果你的室友正在读这本书休息时也用同样的方法。
你和她只需要写下这三个数字,你们就可以轮流一起读这本书。

线程的工作原理类似。
CPU会给你一种错觉,它可以同时执行多个进程,而实际上,它只在每个进程上花费很少的时间——所谓的多线程和并发处理只是一种错觉。
CPU可以做到这一点,因为它对每个任务都有一个执行上下文,就像您可以与朋友分享同一本书一样。

进程和线程的区别:

同一进程内的线程共享相同的内存空间,但进程之间的内存空间是独立的。

同一进程中的所有线程共享数据,但进程之间的数据是独立的。

对父线程的修改可能会影响其他线程的行为,但对父进程的修改(删除除外)不会影响其他子进程。

线程是上下文执行指令,而进程是与进程关联的资源的集合。

同一进程内的线程可以直接通信,但进程之间的通信需要借助中间代理。

创建新线程很容易,但创建新进程需要原始进程的副本。

一个线程可以运行同一进程的其他线程,但一个进程只能运行自己的子进程。

线程启动速度快,进程启动速度慢(但两者的运行速度没有可比性)。

随着现代CPU进入多核时代,主频较过去大幅提升,多线程、多进程编程已成为主流。
Python完全支持多线程和多线程编程,也支持协程。