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个进程,每个进程开启200W线程,理论上运行速度会比单个进程运行200W线程快10倍以上。
不是10倍,而是10倍以上吗?主要原因是切换200W线程的CPU消耗明显比切换20W进程的CPU消耗要多得多。
考虑到这个成本,要高出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,几条记录whileurl!='':iURL添加到协程队列。
+=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多进程编程之Pipe

在Python多进程开发中,要实现进程间资源共享,我们通常依赖Pipe、Queue等机制。
本文将重点介绍Pipes的基本用法,包括其对象的创建、.send()和.recv()等核心方法的使用,并通过示例加深理解。
首先,从multiprocessing包中导入Pipe模块,该模块将返回一对连接对象conn1和conn2,这两个端口默认为全双工模式。
在主进程中,我们创建这个管道并将其作为参数传递给子进程。
主子进程通过这两个端口建立数据交换桥梁。
在主进程中,conn1和conn2都可以进行send和recv操作;子进程也有相同的权限。
通过实际演示,我们可以验证两个进程如何同时处理各自的端口。
请记住,每个端口的.close()方法用于中断对其的进程控制,并且需要在所有关联进程中调用以确保完全关闭。
例如,如果主进程关闭conn2,但子进程继续使用它,则子进程仍然可以通过conn2发送数据,主进程通过conn1接收数据。
但是,如果在子进程中也调用了conn2.close(),则main将不再能够接收数据,这可能会导致程序阻塞。
因此,了解何时关闭端口是关键。
简而言之,Python的Pipe提供了多进程之间共享资源的重要手段。
通过掌握核心方法和使用技巧,开发人员可以在项目中有效地使用Pipe进行数据传输。
通过本文的介绍和示例,希望能够帮助您好地理解和使用这个工具。