精华内容
下载资源
问答
  • 线程、进程、多线程、多进程 和 多任务 小结

    千次阅读 多人点赞 2019-04-20 11:59:56
    3 多进程 4 多线程 5 线程与进程的关系 6 线程和进程的区别 7 进程的优缺点 7.1 进程的优点 7.2 进程的缺点 8 线程的优缺点 8.1 线程的优点 8.2 线程的缺点 9 多线程的优缺点 9.1 多线程的优点 9.2 多...

    目录

    1 进程

    2 线程

    3 多进程

    4 多线程

    5 线程与进程的关系

    6 线程和进程的区别

    7 进程的优缺点

    7.1 进程的优点

    7.2 进程的缺点

    8 线程的优缺点

    8.1 线程的优点

    8.2 线程的缺点

    9 多线程的优缺点

    9.1 多线程的优点

    9.2 多线程的缺点

    10多进程的优缺点

    10.1 多进程的优点

    10.2 多进程的缺点

    8 多任务(多进程)

    参考资料


    最近经常看到 多进程,多线程和多任务等名词,很容易混。网上查了很多资料,内容很多。作为Linux初学者,还是想从最基础的开始了解,找通俗的例子了解,由浅入深。我把网上查阅的资料整理的一下,一次性全部摸透,还是有点难度的。写个博客,记录一下,以便后期查阅复习。

    首先,从定义开始,先看一下教科书上 进程线程定义:

    进程:资源分配的最小单位。

    线程:程序执行的最小单位。

    心中默念,啥啥啥,写的这是啥。于是乎 我就想到王宝强一脸懵逼的表情。

     

    1 进程

    进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

    举例说明进程

    想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(CPU),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机制是从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。

     

    2 线程

    线程是CPU调度的最小单位(程序执行流的最小单元),它被包含在进程之中,是进程中的实际运作单元。一条线程是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

    一个标准的线程有线程ID、当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单元,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现处间断性。

    线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

     

    举例说明线程

    假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西——-文本内容,不停的切换造成性能上的损失。若有一种机制,可以使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。这种机制就是线程

    总的来说进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。

     

     

    3 多进程

    进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。进程可以分为系统进程用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身;所有由用户启动的进程都是用户进程。进程是操作系统进行资源分配的单位。 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多任务。现代的操作系统几乎都是多任务操作系统,能够同时管理多个进程的运行。 多任务带来的好处是明显的,比如你可以边听网易云音乐,一边上网,与此同时甚至可以将下载的文档打印出来,而这些任务之间丝毫不会相互干扰。那么这里就涉及到并行的问题,俗话说,一心不能二用,这对计算机也一样,原则上一个CPU只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用,同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”,它的思想简单介绍如下:在操作系统的管理下,所有正在运行的进程轮流使用CPU,每个进程允许占用CPU的时间非常短(比如10毫秒),这样用户根本感觉不出来 CPU是在轮流为多个进程服务,就好像所有的进程都在不间断地运行一样。但实际上在任何一个时间内有且仅有一个进程占有CPU。 如果一台计算机有多个CPU,情况就不同了,如果进程数小于CPU数,则不同的进程可以分配给不同的CPU来运行,这样,多个进程就是真正同时运行的,这便是并行

    并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行

    并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集。

     

     

    4 多线程

    线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单元。在单一程序中同时运行多个想成完成不同的工作,称为多线程

    多线程是为了使得多个线程并行的工作以完成多项任务,以提高系统的效率。线程是在同一时间需要完成多项任务的时候被实现的。

    打个比方:

    多进程是立体交通系统(近似于立交桥),虽然造价高,上坡下坡多耗点油,但是不堵车。

    多线程是平面交通系统,造价低,但红绿灯太多,老堵车。

     

     

    5 线程与进程的关系

    (1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;

    (2)资源分配给进程,同一进程内的所有线程共享该进程的所有资源;

    (3)线程在执行过程中需要协作同步。不同进程中的线程之间要利用消息通信的方法实现同步;

    (4)处理机分配给线程,即真正在处理机上运行的是线程;

    (5)线程是进程的一个执行单元,也是进程内的可调用实体。

     

     

    6 线程和进程的区别

    (1)线程共享内存空间;进程的内存是独立的;

    (2)同一个进程的线程之间可以直接交流;两个进程想通信,必须通过一个中间代理来实现;

    (3)创建新进程很简单;创建新进程需要对其父进程进行一个克隆;

    (4)一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作子进程;

    (5)改变注线程(如优先权),可能会影响其他线程;改变父进程,不影响子进程。

    (6)调度:线程作为分配和调度的基本单位,进程作为拥有资源的基本单位

    (7)并发性:不进进程之间可以并发执行,同一进程内的线程也可以并发执行

    (8)拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的系统资源

    (9)系统开销:在创建和撤销进程的时候,系统都要分配和回收资源,导致系统的明显大于创建和撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式的下不会对其他进程造成影响,而线程只是进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有独立的地址空间,一个线程死后就等于整个进程死掉,所以多进程程序要比多线程程序健壮,但是在进程切换的时候消耗的资源较大,效率差。

    根本区别就一点:用多进程每个进程有自己的地址空间(address space),线程则共享地址空间。

    总结:多线程执行效率高;  多进程耗资源,安全。

     

     

    7 进程的优缺点

    7.1 进程的优点

    1)顺序程序的特点:具有封闭性和可再现性;

    2)程序的并发执行和资源共享。多道程序设计出现后,实现了程序的并发执行和资源共享,提高了系统的效率和系统的资源利用率。

     

    7.2 进程的缺点

    操作系统调度切换多个线程要比切换调度进程在速度上快的多。而且进程间内存无法共享,通讯也比较麻烦。线程之间由于共享进程内存空间,所以交换数据非常方便;在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。  

     

     

    8 线程的优缺点

    8.1 线程的优点

    1)它是一种非常"节俭"的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码 段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程 所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。当然,在具体的系统上,这个数据可能 会有较大的区别;

    2)线程间方便的通信机制,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便;

    3)使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上;

    4)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

     

    8.2 线程的缺点

    1)调度时, 要保存线程状态,频繁调度,需要占用大量的机时;

    2)程序设计上容易出错(线程同步问题)。

     

     

    9 多线程的优缺点

    9.1 多线程的优点

    1)无需跨进程边界; 程序逻辑和控制方式简单;

    2)所有线程可以直接共享内存和变量等;

    3)线程方式消耗的总资源比进程方式好。

     

    9.2 多线程的缺点

    1)每个线程与主程序共用地址空间,受限于2GB地址空间;

    2)线程之间的同步和加锁控制比较麻烦; 一个线程的崩溃可能影响到整个程序的稳定性;

    3)到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server 2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;

    4)线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU 。

     

    10多进程的优缺点

    10.1 多进程的优点

    1)每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;

    2)通过增加CPU,就可以容易扩充性能;

    3)可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;

    4)每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。

     

    10.2 多进程的缺点

    1)逻辑控制复杂,需要和主程序交互;

    2)需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大。

     

    总结:最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+CPU+轮询方式来解决问题……方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。

     

    按照多个不同的维度(类别),来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。

        对比类别

                                        多进程

                                     多线程

         总结

    数据共享、同步

    数据共享复杂,需要用IPC;数据是分开的,同步简单

    因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂

     各有优势

        内存、CPU

    占用内存多,切换复杂,CPU利用率低

    占用内存少,切换简单,CPU利用率高

     线程占优

     创建销毁、切换

    创建销毁、切换复杂,速度慢

    创建销毁、切换简单,速度很快

     线程占优

        编程、调试

    编程简单,调试简单

    编程复杂,调试复杂

     进程占优

           可靠性

    进程间不会互相影响

    一个线程挂掉将导致整个进程挂掉

     进程占优

           分布式

    适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单

    适应于多核分布式

     进程占优

    其实没有绝对的好与坏,只有哪个更加合适的问题。我们来看实际应用中究竟如何判断更加合适。

    1)需要频繁创建销毁的优先用线程

    这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

     

    2)需要进行大量计算的优先使用线程

    所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。这种原则最常见的是图像处理、算法处理。

     

    3)强相关的处理用线程,弱相关的处理用进程

    什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

    一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

     

    4)可能要扩展到多机分布的用进程,多核分布的用线程

     

    5)都满足需求的情况下,用你最熟悉、最拿手的方式

    至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。

     

    8 多任务(多进程)

    现代操作系统比如Mac OS X,UNIX,Linux,Windows等,都是支持“多任务”的操作系统。

    什么叫“多任务”呢?简单地说,就是操作系统可以同时运行多个任务。打个比方,你一边在用浏览器上网,一边在听MP3,一边在用Word写论文,这就是多任务,至少同时有3个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是桌面上没有显示而已。

    现在,多核CPU已经非常普及了,但是,即使过去的单核CPU,也可以执行多任务。由于CPU执行代码都是顺序执行的,那么,单核CPU是怎么执行多任务的呢

    其实操作系统轮流让各个任务交替执行,任务1执行0.01秒,切换到任务2,任务2执行0.01秒,再切换到任务3,执行0.01秒……这样反复执行下去。表面上看,每个任务都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像所有任务都在同时执行一样。

    真正的并行执行多任务只能在多核CPU上实现,但是,由于任务数量远远多于CPU的核心数量,所以,操作系统也会自动把很多任务轮流调度到每个核心上执行。

    对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。

    有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些 “子任务” 称为线程(Thread)。

    由于每个进程至少要干一件事,所以,一个进程至少有一个线程。当然,像Word这种复杂的进程可以有多个线程,多个线程可以同时执行,多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

     

     

    参考资料

    [1] https://www.cnblogs.com/pingqiang/p/8007549.html#多进程多线程

    [2] https://www.cnblogs.com/ww36315610/p/3881533.html?tdsourcetag=s_pctim_aiomsg

    [3] http://www.cnblogs.com/Yogurshine/p/3640206.html

    [4] https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013868322563729e03f6905ea94f0195528e3647887415000?tdsourcetag=s_pctim_aiomsg

    [5] https://www.cnblogs.com/yuanchenqi/articles/6755717.html#_label3?tdsourcetag=s_pctim_aiomsg

    [6] https://www.cnblogs.com/zhanht/p/5401685.html?tdsourcetag=s_pctim_aiomsg

    [7] https://www.kafan.cn/edu/85866456.html

     

    展开全文
  • python多进程多线程,多个程序同时运行

    千次阅读 多人点赞 2021-04-08 13:47:15
    python 多线程 多进程同时运行 多任务要求 python 基础语法 python 文件目录操作 python 模块应用 开发工具 pycharm 实现方法 多任务的实现可以用进程和线程来实现 进程—> 线程----> 多任务应用 多进程操作...

    python 多线程 多进程同时运行

    多任务要求

    1. python 基础语法
    2. python 文件目录操作
    3. python 模块应用

    开发工具

    pycharm

    实现方法

    多任务的实现可以用进程和线程来实现

    进程—> 线程----> 多任务应用

    多进程操作

    比如下载多个文件, 利用cpu 资源 提高效率

    多任务: 同一时间执行多个任务, 比如windows操作系统

    执行方式有两种( 表现形式 )

    并发

    在单核cpu中:

    在一段时间内交替执行多个任务, 例如单核cpu 处理多任务, 操作系统让各个任务交替进行

    并行

    多核cpu 给每个核都分配一个任务 每个核执行自己的任务

    真正意义上的同时执行

    如何实现( 原理 )

    多进程

    python 中实现多进程来实现多任务, 进程是操作系统进行资源分配和调度的最小单位, 一个运行起来的程序叫做进程, 没有在运行的程序叫做程序

    当一个.py 文件运行的时候系统会创建一个主进程

    比如

    def fun1():
        print(1)
    def fun2():
        print(2)
        
    fun1()
    fun2()
    

    代码运行的时候系统会先执行fun1 在执行fun2 按顺序运行

    使用多进程的话: 在程序运行的时候会创建一个主进程, 那么在程序运行除了主进程之外创建一个子进程, 我们让主进程去执行fun1(), 让子进程去执行fun2(), 让他们两个同时执行, 来实现多任务同时进行

    多进程完成多任务

    首先要知道如何去创建第二个进程, 第三个进程, 进程的创建有三个步骤

    1. 导入进程包
      • import multiprocessing
    2. 通过进程类去创建进程对象
      • 进程对象=multiprocessing.Process()
    3. 启动进程执行任务
      • 进程对象.start() 来开始执行进程
    import threading
    import time
    import os
    def fun1():
        for i in range(3):
            print('1')
            time.sleep(1)
    
    def fun2():
        for i in range(3):
            print('2')
            time.sleep(3)
    
    
    if __name__ == '__main__':
        sing_thread = threading.Thread(target=fun1)
        song_thread = threading.Thread(target=fun2)
    
        sing_thread.start()
        song_thread.start()
    

    通过进程类创建进程对象

    进程对象 = multiprocessing.Process(target= 任务名)

    参数名说明
    target执行的目标任务名, 这里指的是函数名(方法名)
    name进程名, 一般不用设置(系统自动为进程设置默认名字Process-1····)
    group进程组, 目前只能使用None

    必须要指定进程的名字target

    def none():
        pass
       
    process = multiprocessing.Process(target=none)
    process.start()
    

    进程如何执行带有参数的任务

    参数名说明
    args以元祖的方式给任务传递参数, 顺序要正确
    kwrags以字典的方式给执行任务传递参数, key 跟参数名要一致

    process = multiprocessing.Process(target=function, args=(10,)) arg传递的必须是参数所以传递一个变量的时候需要(10,)

    process = multiprocessing.Process(target=function, kwargs={'key': value})

    import multiprocessing
    import time
    import os
    
    def fun1(num):
        print('1进程PID', os.getpid())
        print('1进程父进程编号', os.getppid())
        for i in range(num):
            print('1....\n')
            time.sleep(0.5)
    
    
    def fun2(num):
        print('2进程PID', os.getpid())
        print('2进程父进程编号', os.getppid())
        for i in range(num):
            print('2...')
            time.sleep(0.5)
    
    # 1. 导入进程包
    
    if __name__ == '__main__':
        sing_process = multiprocessing.Process(target=fun1, args=(5,))
        dance_process = multiprocessing.Process(target=fun2, kwargs={'num':2}) # target
    
        #3. 启动进程
        sing_process.start()
        dance_process.start()
    

    获取进程编号

    1. 获取当前进程的编号
      • os.getpid()
    2. 获取当前父进程的编号
      • os.getppid()
    import os 
    os.getpid()
    def work():
        print('work 进程编号', os.getpid())
        print('work 父进程编号', os.getppid())
        
    

    主进程默认不会结束, 在子进程全部结束之后才会关闭主进程

    在实际中我们想要的效果, 主进程关闭,, 子进程全部关闭

    设置守护主进程

    希望主进程关闭的时候子进程不在执行. 则需要设置守护选项

    子进程对象.daemon=True 那么在主进程运行完成之后, 那么子进程也将会自动结束

    os 部分操作

    os 创建目标文件夹目录

    os.mkdir(dir)

    多线程

    实现多任务的另一种形式, 进程是分配资源的最小单位, 线程是程序执行的最小单位, 一个进程中最少有一个线程来负责该进程, 比如一个QQ(一个进程) 打开了两个窗口(两个线程)跟两个人聊天, 线程自己并不需要系统资源, 线程与其他线程共享这个进程的资源, 因此使用多线程的时候实现了资源节约,

    1. 多线程是Python 程序中实现多任务的一种方式
    2. 线程是程序执行的最小单位
    3. 同属于一个进程的多线程共享进程拥有的所有资源

    多线程实现多任务

    线程的创建步骤

    1. 导入线程模块
      • import threading
    2. 通过线程类去创建线程对象
      • 线程对象=threading.Thread(target=任务名)
    3. 启动线程执行任务
      • 线程对象.start()
    参数名说明
    target执行目标的任务名, 这里值函数名( 方法名)
    name线程名, 一般不用设置
    group线程组, 目前只能使用None

    其他的一毛一样

    进程线程对比

    关系对比

    1. 线程是依附在进程里边的, 没有进程就没有线程
    2. 一个进程默认提供一条线程, 进程可以创建多个线程

    区别对比

    1. 进程创建的开销要比线程的开销要大
    2. 进程是操作系统资源分配的基本单位, 线程是cpu 调度的基本单位
    3. 线程不能独立执行, 必须依存进程

    优缺点对比

    1. 进程优缺点:

      优点:可以用多核

      缺点:资源开销大

    2. 线程优缺点

      优点: 资源开销小

      缺点:不能使用多核

    本笔记总结于bilibili 黑马程序员
    多进程多线程视频

    展开全文
  • Python多进程和多线程(跑满CPU)

    万次阅读 多人点赞 2019-05-07 15:06:16
    Python多进程和多线程(跑满CPU) 概念 任务可以理解为进程(process),如打开一个word就是启动一个word进程。在一个word进程之中不只是进行打字输入,还需要拼写检查、打印等子任务,我们可以把进程中的这些子...

    参考资料:

    https://www.liaoxuefeng.com/wiki/1016959663602400/1017627212385376

    Python多进程和多线程(跑满CPU

    • 概念

    任务可以理解为进程(process),如打开一个word就是启动一个word进程。在一个word进程之中不只是进行打字输入,还需要拼写检查、打印等子任务,我们可以把进程中的这些子任务称为线程(thread)。

    由于每个进程至少要干一件事,那么一个进程至少有一个线程,有时候有的复杂进程有多个线程,在进程中的多个线程是可以同时执行的。多线程的执行方式和多进程是一样的,也是由操作系统在多个线程之间快速切换,让每个线程都短暂地交替运行,看起来就像同时执行一样。当然,真正地同时执行多线程需要多核CPU才可能实现。

     

    总结一下就是,多任务的实现有3种方式:

    • 多进程模式;

    启动多个进程,每个进程虽然只有一个线程,但多个进程可以一块执行多个任务。

    • 多线程模式

    启动一个进程,在一个进程内启动多个线程,这样,多个线程也可以一块执行多个任务。

    • 多进程+多线程模式。

     

    启动多个进程,每个进程再启动多个线程,这样同时执行的任务就更多了,当然这种模型更复杂,实际很少采用。同时执行多个任务通常各个任务之间并不是没有关联的,而是需要相互通信和协调,有时,任务1必须暂停等待任务2完成后才能继续执行,有时,任务3和任务4又不能同时执行,所以,多进程和多线程的程序的复杂度要远远高于我们前面写的单进程单线程的程序。

     

    • 多进程

    要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识。Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

    子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。

    multiprocessing模块提供了一个Process类来代表一个进程对象。

    创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

             multiprocessing模块提供了一个Pool进程池的方式批量创建子进程。

     

    from multiprocessing import Pool
    
    import os, time
    
    
    def long_time_task(name):
    
        print('Run task %s (%s)...' % (name, os.getpid()))
    
        start = time.time()
    
        time.sleep(1)
    
        end = time.time()
    
        print('Task %s runs %0.2f seconds.' % (name, (end - start)))
    
    if __name__=='__main__':
    
        print('Parent process %s.' % os.getpid())
    
        p = Pool(5)
    
        for i in range(10):
    
            p.apply_async(long_time_task, args=(i,))
    
        print('Waiting for all subprocesses done...')
    
        p.close()
    
        p.join()
    
        print('All subprocesses done.')

    执行结果:

    Parent process 13780.
    
    Waiting for all subprocesses done...
    
    Run task 0 (9208)...
    
    Run task 1 (5052)...
    
    Run task 2 (15320)...
    
    Run task 3 (13604)...
    
    Run task 4 (1408)...
    
    Task 0 runs 1.00 seconds.
    
    Run task 5 (9208)...
    
    Task 1 runs 1.00 seconds.
    
    Run task 6 (5052)...
    
    Task 2 runs 1.00 seconds.
    
    Run task 7 (15320)...
    
    Task 3 runs 1.00 seconds.
    
    Run task 8 (13604)...
    
    Task 4 runs 1.00 seconds.
    
    Run task 9 (1408)...
    
    Task 5 runs 1.00 seconds.
    
    Task 6 runs 1.00 seconds.
    
    Task 7 runs 1.00 seconds.
    
    Task 8 runs 1.00 seconds.
    
    Task 9 runs 1.00 seconds.
    
    All subprocesses done.

    使用Pool对象,Pool中的参数表示调用多少个并行进程进行程序运行,pool的默认容量为CPU的计算核的数量,我们可以这样理解pool池:

    直观上的理解为指定了可以容纳最多多少个进程进行并行运算。而p.apply_async()函数给进程池添加进程任务如上图的Process1, Process2 … ,它的参数包括待运行程序和程序的传入参数。对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。

    请注意输出的结果,process 01234是立刻执行的,而process  5要等待前面某个process完成后才执行,这是因为Pool的大小为5,在电脑上默认为CPU的核数。因此,最多同时执行5个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:

    p = Pool(10)就可以同时跑10个进程。

     

    • 子进程

    很多时候,子进程并不是自身,而是一个外部进程。我们创建了子进程后,还需要控制子进程的输入和输出。Subprocess模块可以让我们非常方便地启动一个子进程,然后控制其输入和输出。如果子进程还需要输入,则可以通过communicate()方法输入:

     

    • 进程间的通信

    Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Pythonmultiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

    我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

    from multiprocessing import Process, Queue
    
    import os, time, random
    
    
    # 写数据进程执行的代码:
    
    def write(q):
    
        print('Process to write: %s' % os.getpid())
    
        for value in ['A', 'B', 'C']:
    
            print('Put %s to queue...' % value)
    
            q.put(value)
    
            time.sleep(random.random())
    
    
    # 读数据进程执行的代码:
    
    def read(q):
    
        print('Process to read: %s' % os.getpid())
    
        while True:
    
            value = q.get(True)
    
            print('Get %s from queue.' % value)
    
    
    if __name__=='__main__':
    
        # 父进程创建Queue,并传给各个子进程:
    
        q = Queue()
    
        pw = Process(target=write, args=(q,))
    
        pr = Process(target=read, args=(q,))
    
        # 启动子进程pw,写入:
    
        pw.start()
    
        # 启动子进程pr,读取:
    
        pr.start()
    
        # 等待pw结束:
    
        pw.join()
    
        # pr进程里是死循环,无法等待其结束,只能强行终止:
    
        pr.terminate()

     

    运行结果:

    Process to write: 15728
    
    Put A to queue...
    
    Process to read: 15748
    
    Get A from queue.
    
    Put B to queue...
    
    Get B from queue.
    
    Put C to queue...
    
    Get C from queue.

    Pipes如何使用?

     

    • 多线程

    多任务可以由多进程完成,也可以由一个进程内的多线程完成。一个进程至少有一个线程,由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。Python的标准库提供了两个模块:_threadthreading_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

    import time, threading
    
    
    # 新线程执行的代码:
    
    def loop():
    
        print('thread %s is running...' % threading.current_thread().name)
    
        n = 0
    
        while n < 5:
    
            n = n + 1
    
            print('thread %s >>> %s' % (threading.current_thread().name, n))
    
            time.sleep(1)
    
        print('thread %s ended.' % threading.current_thread().name)
    
    
    print('thread %s is running...' % threading.current_thread().name)
    
    t = threading.Thread(target=loop, name='LoopThread')
    
    t.start()
    
    t.join()
    
    print('thread %s ended.' % threading.current_thread().name)

    执行结果:

    thread MainThread is running...
    
    thread LoopThread is running...
    
    thread LoopThread >>> 1
    
    thread LoopThread >>> 2
    
    thread LoopThread >>> 3
    
    thread LoopThread >>> 4
    
    thread LoopThread >>> 5
    
    thread LoopThread ended.
    
    thread MainThread ended.

    由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1Thread-2……。在上述程序中我们的主线程是整个程序,子线程执行loop()函数,输出的LoopThread是子线程的名字,输出的1,2,3,… 不是线程的数量。

    Lock:

    多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。(对于变量:进程à复制;线程à共享)

    对于多线程将程序变量改乱了的例子:

    import time, threading
    
    
    # 假定这是你的银行存款:
    
    balance = 0
    
    
    def change_it(n):
    
        # 先存后取,结果应该为0:
    
        global balance
    
        balance = balance + n
    
        balance = balance - n
    
    
    def run_thread(n):
    
        for i in range(100000):
    
            change_it(n)
    
    
    t1 = threading.Thread(target=run_thread, args=(5,))
    
    t2 = threading.Thread(target=run_thread, args=(8,))
    
    t1.start()
    
    t2.start()
    
    t1.join()
    
    t2.join()
    
    print(balance)

    我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:

    Balance = Balance + n

    都需要分两步计算:

    1. 计算临时变量temp = Balance + n
    2. 将临时变量temp赋值给Balance

    当两个线程正常顺序工作时,并不会造成冲突,但是,如果两个线程中的CPU执行语句不是按照线程顺序执行时有可能会造成冲突:

    初始值 balance = 0

    t1: x1 = balance + 5  # x1 = 0 + 5 = 5

    t2: x2 = balance + 8  # x2 = 0 + 8 = 8

    t2: balance = x2      # balance = 8

    t1: balance = x1      # balance = 5

    t1: x1 = balance - 5  # x1 = 5 - 5 = 0

    t1: balance = x1      # balance = 0

    t2: x2 = balance - 8  # x2 = 0 - 8 = -8

    t2: balance = x2   # balance = -8

    结果 balance = -8

    究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。

     

    两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。

    如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:

    balance = 0
    
    lock = threading.Lock()
    
    
    def run_thread(n):
    
        for i in range(100000):
    
            # 先要获取锁:
    
            lock.acquire()
    
            try:
    
                # 放心地改吧:
    
                change_it(n)
    
            finally:
    
                # 改完了一定要释放锁:
    
                lock.release()

    当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

    获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

    锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

     

    • 多核CPU

    如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。如果写一个死循环的话,会出现什么情况呢?打开Mac OS X的Activity Monitor,或者Windows的Task Manager,都可以监控某个进程的CPU使用率。我们可以监控到一个死循环线程会100%占用一个CPU。如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。试试用Python写个死循环:

    import threading, multiprocessing
    
    
    def loop():
    
        x = 0
    
        while True:
    
            x = x ^ 1
    
    
    for i in range(multiprocessing.cpu_count()):
    
        t = threading.Thread(target=loop)
    
    t.start()

     

    程序:

    进程:

    资源监视器:

     

    启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

     

    • 进程VS.线程

    我们介绍了多进程和多线程,这是实现多任务最常用的两种方式。现在,我们来讨论一下这两种方式的优缺点。

    首先,要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务,因此,多任务环境下,通常是一个Master,多个Worker。

    如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。

    如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。

    多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。

    多进程模式的缺点是创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。

    多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。在Windows上,如果一个线程执行的代码出了问题,你经常可以看到这样的提示:“该程序执行了非法操作,即将关闭”,其实往往是某个线程出了问题,但是操作系统会强制结束整个进程。

    在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式,真是把问题越搞越复杂。

    线程切换:

    无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。

    如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。

    假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。

    但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。

    所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。

     

    计算密集型 vs. IO密集型:

    是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密集型IO密集型

    计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。

    计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

    第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。

    IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

    异步IO

    考虑到CPU和IO之间巨大的速度差异,一个任务在执行的过程中大部分时间都在等待IO操作,单进程单线程模型会导致别的任务无法并行执行,因此,我们才需要多进程模型或者多线程模型来支持多任务并发执行。

    现代操作系统对IO操作已经做了巨大的改进,最大的特点就是支持异步IO。如果充分利用操作系统提供的异步IO支持,就可以用单进程单线程模型来执行多任务,这种全新的模型称为事件驱动模型,Nginx就是支持异步IO的Web服务器,它在单核CPU上采用单进程模型就可以高效地支持多任务。在多核CPU上,可以运行多个进程(数量与CPU核心数相同),充分利用多核CPU。由于系统总的进程数量十分有限,因此操作系统调度非常高效。用异步IO编程模型来实现多任务是一个主要的趋势。

    对应到Python语言,单线程的异步编程模型称为协程,有了协程的支持,就可以基于事件驱动编写高效的多任务程序。

    • 小结

    多线程编程,模型复杂,容易发生冲突,必须用锁加以隔离,同时,又要小心死锁的发生。Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。

    • 分布式进程

    ThreadProcess中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上。

    Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上。一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。

    举个例子:如果我们已经有一个通过Queue通信的多进程程序在同一台机器上运行,现在,由于处理任务的进程任务繁重,希望把发送任务的进程和处理任务的进程分布到两台机器上。怎么用分布式进程实现?

    原有的Queue可以继续使用,但是,通过managers模块把Queue通过网络暴露出去,就可以让其他机器的进程访问Queue了。

    我们先看服务进程,服务进程负责启动Queue,把Queue注册到网络上,然后往Queue里面写入任务:

    # task_master.py
    
    
    import random, time, queue
    
    from multiprocessing.managers import BaseManager
    
    
    # 发送任务的队列:
    
    task_queue = queue.Queue()
    
    # 接收结果的队列:
    
    result_queue = queue.Queue()
    
    # 从BaseManager继承的QueueManager:
    
    class QueueManager(BaseManager):
    
        pass
    
    # 把两个Queue都注册到网络上, callable参数关联了Queue对象:
    
    QueueManager.register('get_task_queue', callable=lambda: task_queue)
    
    QueueManager.register('get_result_queue', callable=lambda: result_queue)
    
    # 绑定端口5000, 设置验证码'abc':
    
    manager = QueueManager(address=('', 5000), authkey=b'abc')
    
    # 启动Queue:
    
    manager.start()
    
    # 获得通过网络访问的Queue对象:
    
    task = manager.get_task_queue()
    
    result = manager.get_result_queue()
    
    # 放几个任务进去:
    
    for i in range(10):
    
        n = random.randint(0, 10000)
    
        print('Put task %d...' % n)
    
        task.put(n)
    
    # 从result队列读取结果:
    
    print('Try get results...')
    
    for i in range(10):
    
        r = result.get(timeout=10)
    
        print('Result: %s' % r)
    
    # 关闭:
    
    manager.shutdown()
    
    print('master exit.')
    
    

    请注意,当我们在一台机器上写多进程程序时,创建的Queue可以直接拿来用,但是,在分布式多进程环境下,添加任务到Queue不可以直接对原始的task_queue进行操作,那样就绕过了QueueManager的封装,必须通过manager.get_task_queue()获得的Queue接口添加。然后,在另一台机器上启动任务进程(本机上启动也可以):

    # task_worker.py
    
    import time, sys, queue
    
    from multiprocessing.managers import BaseManager
    
    # 创建类似的QueueManager:
    
    class QueueManager(BaseManager):
    
        pass
    
    # 由于这个QueueManager只从网络上获取Queue,所以注册时只提供名字:
    
    QueueManager.register('get_task_queue')
    
    QueueManager.register('get_result_queue')
    
    # 连接到服务器,也就是运行task_master.py的机器:
    
    server_addr = '127.0.0.1'
    
    print('Connect to server %s...' % server_addr)
    
    # 端口和验证码注意保持与task_master.py设置的完全一致:
    
    m = QueueManager(address=(server_addr, 5000), authkey=b'abc')
    
    # 从网络连接:
    
    m.connect()
    
    # 获取Queue的对象:
    
    task = m.get_task_queue()
    
    result = m.get_result_queue()
    
    # 从task队列取任务,并把结果写入result队列:
    
    for i in range(10):
    
        try:
    
            n = task.get(timeout=1)
    
            print('run task %d * %d...' % (n, n))
    
            r = '%d * %d = %d' % (n, n, n*n)
    
            time.sleep(1)
    
            result.put(r)
    
        except Queue.Empty:
    
            print('task queue is empty.')
    
    # 处理结束:
    
    print('worker exit.')

    任务进程要通过网络连接到服务进程,所以要指定服务进程的IP。

    现在,可以试试分布式进程的工作效果了。先启动task_master.py服务进程:

    $ python3 task_master.py

    Put task 3411...

    Put task 1605...

    Put task 1398...

    Put task 4729...

    Put task 5300...

    Put task 7471...

    Put task 68...

    Put task 4219...

    Put task 339...

    Put task 7866...

    Try get results...

    task_master.py进程发送完任务后,开始等待result队列的结果。现在启动task_worker.py进程:

    $ python3 task_worker.py

    Connect to server 127.0.0.1...

    run task 3411 * 3411...

    run task 1605 * 1605...

    run task 1398 * 1398...

    run task 4729 * 4729...

    run task 5300 * 5300...

    run task 7471 * 7471...

    run task 68 * 68...

    run task 4219 * 4219...

    run task 339 * 339...

    run task 7866 * 7866...

    worker exit.

    task_worker.py进程结束,在task_master.py进程中会继续打印出结果:

    Result: 3411 * 3411 = 11634921

    Result: 1605 * 1605 = 2576025

    Result: 1398 * 1398 = 1954404

    Result: 4729 * 4729 = 22363441

    Result: 5300 * 5300 = 28090000

    Result: 7471 * 7471 = 55815841

    Result: 68 * 68 = 4624

    Result: 4219 * 4219 = 17799961

    Result: 339 * 339 = 114921

    Result: 7866 * 7866 = 61873956

    这个简单的Master/Worker模型有什么用?其实这就是一个简单但真正的分布式计算,把代码稍加改造,启动多个worker,就可以把任务分布到几台甚至几十台机器上,比如把计算n*n的代码换成发送邮件,就实现了邮件队列的异步发送。

    Queue对象存储在哪?注意到task_worker.py中根本没有创建Queue的代码,所以,Queue对象存储在task_master.py进程中:

    Queue之所以能通过网络访问,就是通过QueueManager实现的。由于QueueManager管理的不止一个Queue,所以,要给每个Queue的网络调用接口起个名字,比如get_task_queue

    authkey有什么用?这是为了保证两台机器正常通信,不被其他机器恶意干扰。如果task_worker.pyauthkeytask_master.pyauthkey不一致,肯定连接不上。

    小结:

    Python的分布式进程接口简单,封装良好,适合需要把繁重任务分布到多台机器的环境下。

    注意Queue的作用是用来传递任务和接收结果,每个任务的描述数据量要尽量小。比如发送一个处理日志文件的任务,就不要发送几百兆的日志文件本身,而是发送日志文件存放的完整路径,由Worker进程再去共享的磁盘上读取文件。

    展开全文
  • 正常情况下,一个apk启动后只会运行在一个进程中,其进程名为apk的包名,所有的组件都会在这个进程中运行,以下为DDMS的进程截屏: com.biyou.multiprocess为进程名,也是apk的包名,  但是如果需要将某些组件(如...

    前言

    正常情况下,一个apk启动后只会运行在一个进程中,其进程名为apk的包名,所有的组件都会在这个进程中运行,以下为DDMS的进程截屏:

    这里写图片描述

    com.biyou.multiprocess为进程名,也是apk的包名, 


    但是如果需要将某些组件(如Service,Activity等)运行在单独的进程中,就需要用到android:process属性了。我们可以给android的组件设置android:process属性来使其运行在指定的进程中。

    • AndroidMantifest.xml中的activity、service、receiver和provider均支持android:process属性
    • 设置该属性可以使每个组件均在各自的进程中运行,或者使一些组件共享一个进程
    • AndroidMantifest.xml中的application元素也支持android:process属性,可以修改应用程序的默认进程名(默认值为包名)

    为何要使用多进程

     

    1.分散内存的占用



    我们知道Android系统对每个应用进程的内存占用是有限制的,而且占用内存越大的进程,通常被系统杀死的可能性越大。让一个组件运行在单独的进程中,可以减少主进程所占用的内存,避免OOM问题,降低被系统杀死的概率,

    2.实现多模块



    比如我做的应用大而全,里面肯定会有很多模块,假如有地图模块、大图浏览、自定义WebView等等(这些都是吃内存大户),还会有一些诸如下载服务,监控服务等等,一个成熟的应用一定是多模块化的。

    当我们的应用开发越来越大,模块越来越多,团队规模也越来越大,协作开发也是个很麻烦的事情。项目解耦,模块化,是这阶段的目标。通过模块解耦,开辟新的进程,独立的JVM,来达到数据解耦目的。模块之间互不干预,团队并行开发,责任分工也明确。

    3.子进程奔溃,主进程可以继续工作

    如果子进程因为某种原因崩溃了,不会直接导致主程序的崩溃,可以降低我们程序的崩溃率。 
     

    4.主进程退出,子进程可以继续工作

    即使主进程退出了,我们的子进程仍然可以继续工作,假设子进程是推送服务,在主进程退出的情况下,仍然能够保证用户可以收到推送消息。 
     

    5.实现守护进程



    如果主线程中的服务要从开机起持续运行,若由于内存等原因被系统kill掉,守护进程可以重新启动主线程的服务。

    通过JNI利用C/C++,调用fork()方法来生成子进程,一般开发者会利用这种方法来做一些daemon(守护进程)进程,来实现防杀保活等效果。

    另外:

    还能通过监控进程,将这个错误上报给系统,告知他在什么机型、环境下、产生了什么样的Bug,提升用户体验。

    实现

    1 . 如果android:process的值以冒号开头的话,那么该进程就是私有进程,如下:

    配置:

    <application
       ……
       <service android:name=".ProcessTestService" android:process=":secondProcess"/>
       ……
    </application>

    进程:

    这里写图片描述

    2 . 以小写字母开头(如com.secondProcess),那么就是公有进程,android:process值一定要有个点号:

    不能以数字开头,并且要符合命名规范,必须要有.否则将会出现这种错误: Invalid process name simon in package com.wind.check: must have at least one ‘.’ 
    配置:

    <application
       ……
       <service android:name=".LocalService" android:process="com.secondProcess"/>
       ……
    </application>

    进程:

    这里写图片描述

    3 . 私有进程和公有进程的区别: 
    android:process=":remote",以冒号开头,冒号后面的字符串原则上是可以随意指定的。如果我们的包名为“com.biyou.multiprocess”,则实际的进程名 
    为“com.biyou.multiprocess:remote”。这种设置形式表示该进程为当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。 
    全局进程 
    进程名称不以“:”开头的进程都可以叫全局进程,如android:process="com.secondProcess",以小写字母开头,表示运行在一个以这个名字命名的全局进程中,其他应用通过设置相同的ShareUID可以和它跑在同一个进程

    ps:ShareUID : 
    ShareUserId,在Android里面每个app都有一个唯一的linux user ID,则这样权限就被设置成该应用程序的文件只对该用户可见,只对该应用程序自身可见,而我们可以使他们对其他的应用程序可见,这会使我们用到SharedUserId,也就是让两个apk使用相同的userID,这样它们就可以看到对方的文件。为了节省资源,具有相同ID的apk也可以在相同的linux进程中进行(注意,并不是一定要在一个进程里面运行),共享一个虚拟机。 
    ShareUserId的作用,数据共享、调用其他程序资源。

    进程生命周期与优先级

    Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。

    重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

    1.前台进程:(foreground process) 

    用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程: 
    托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法) 
    托管某个 Service,后者绑定到用户正在交互的 Activity 
    托管正在“前台”运行的 Service(服务已调用 startForeground()) 
    托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy()) 
    托管正执行其 onReceive() 方法的 BroadcastReceiver 
    通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

    2.可见进程

    没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程: 
    托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。 
    托管绑定到可见(或前台)Activity 的 Service。 
    可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

    3.服务进程 
    正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

    4.后台进程 
    包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。 有关保存和恢复状态的信息,请参阅 Activity文档。

    5.空进程 
    不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。 
    根据进程中当前活动组件的重要程度,Android 会将进程评定为它可能达到的最高级别。例如,如果某进程托管着服务和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。

    此外,一个进程的级别可能会因其他进程对它的依赖而有所提高,即服务于另一进程的进程其级别永远不会低于其所服务的进程。 例如,如果进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者如果进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。

    由于运行服务的进程其级别高于托管后台 Activity 的进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是简单地创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备“服务进程”优先级。 同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

    注意的地方

    有一点一定要记住:进程间的内存空间是不可见的。从而,开启多进程后,我们需要面临这样几个问题:

    1. Application的多次重建。

    Manifest文件如上面提到的,定义了两个类:ProcessTestActivity和ProcessTestService,我们只是在Activity的onCreate方法中直接启动了该Service,同时,我们自定义了自己的Application类。代码如下:

    public class MyApplication extends Application {
        public static final String TAG = "viclee";
        @Override
        public void onCreate() {
            super.onCreate();
            int pid = android.os.Process.myPid();
            Log.d(TAG, "MyApplication onCreate");
            Log.d(TAG, "MyApplication pid is " + pid);
        }
    }
    public class ProcessTestActivity extends Activity {
        public final static String TAG = "viclee";
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_process_test);
    
            Log.i(TAG, "ProcessTestActivity onCreate");
            this.startService(new Intent(this, ProcessTestService.class));
        }
    }
    public class ProcessTestService extends Service {
        public static final String TAG = "viclee";
    
        @Override
        public void onCreate() {
            Log.i(TAG, "ProcessTestService onCreate");
        }
    
        @Override
        public IBinder onBind(Intent arg0) {
            return null;
        }
    
    }


     

    这里写图片描述 
     

     

      我们发现MyApplication的onCreate方法调用了两次,分别是在启动ProcessTestActivity和ProcessTestService的时候,而且我们发现打印出来的pid也不相同。由于通常会在Application的onCreate方法中做一些全局的初始化操作,它被初始化多次是完全没有必要的。出现这种情况,是由于即使是通过指定process属性启动新进程的情况下,系统也会新建一个独立的虚拟机,自然需要重新初始化一遍Application。那么怎么来解决这个问题呢? 
    下面给出解决方案:

    思路:判断是否为主进程,只有主进程的时候才执行下面的操作

    String processName = this.getProcessName();
    
    //判断进程名,保证只有主进程运行
    if (!TextUtils.isEmpty(processName) &&processName.equals(this.getPackageName())) {
        //在这里进行主进程初始化逻辑操作                          
        Log.i(">>>>>>","oncreate");
    }

    获取进程名的方法,这个方法是效率最好的:

     public static String getProcessName() {
            try {
                File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline");
                BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));
                String processName = mBufferedReader.readLine().trim();
                mBufferedReader.close();
                return processName;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }

    2. 静态成员的失效。

    将之前定义的Activity和Service的代码进行简单的修改,代码如下:

    public class ProcessTestActivity extends Activity {
        public final static String TAG = "viclee";
        public static boolean processFlag = false;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_process_test);
    
            processFlag = true;
            Log.i(TAG, "ProcessTestActivity onCreate");
            this.startService(new Intent(this, ProcessTestService.class));
        }
    }
    public class ProcessTestService extends Service {
        public static final String TAG = "viclee";
    
        @Override
        public void onCreate() {
            Log.i(TAG, "ProcessTestService onCreate");
            Log.i(TAG, "ProcessTestActivity.processFlag is " + ProcessTestActivity.processFlag);
        }
    
        @Override
        public IBinder onBind(Intent arg0) {
            return null;
        }
    
    }
    

    重新执行代码,打印Log :
     

    这里写图片描述

     

      从上面的代码和执行结果看,我们在Activity中定义了一个标志processFlag并在onCreate中修改了它的值为true,然后启动Service,但是在Service中读到这个值却为false。按照正常的逻辑,静态变量是可以在应用的所有地方共享的,但是设置了process属性后,产生了两个隔离的内存空间,一个内存空间里值的修改并不会影响到另外一个内存空间。

    3. 文件共享问题。

      多进程情况下会出现两个进程在同一时刻访问同一个数据库文件的情况。这就可能造成资源的竞争访问,导致诸如数据库损坏、数据丢失等。在多线程的情况下我们有锁机制控制资源的共享,但是在多进程中比较难,虽然有文件锁、排队等机制,但是在Android里很难实现。解决办法就是多进程的时候不并发访问同一个文件,比如子进程涉及到操作数据库,就可以考虑调用主进程进行数据库的操作。

    参考: 
    1. 这可能是最全的Android:Process (进程)讲解了 
    2. Android应用内多进程分析和研究

    展开全文
  • Python 多进程及进程间通信

    千次阅读 2020-09-03 21:32:33
    在了解进程之前,我们需要知道任务的概念。任务,顾名思义,就是指操作系统能够执行个任务。例如,使用 Windows 或 Linux 操作系统可以同时看电影、聊天、听音乐等等,此时操作系统就是在执行任务,而每个...
  • Python实现多进程间通信的方法总结

    千次阅读 2020-01-19 03:32:35
    本文全面总结Python中进程间通信的各种方法及经验。
  • 多线程与多进程

    千次阅读 2019-02-27 10:26:49
    多进程 多线程 优劣 数据共享、同步 数据是分开的:共享复杂,需要用IPC;同步简单 多线程共享进程数据:共享简单;同步复杂 各有优势 内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少...
  • 多进程详细讲解

    千次阅读 2018-08-18 19:48:32
    一、什么是多进程 相信看过我其他博客的同学已经对多线程和多进程有一个简单的了解了,下面我们简单说一下多进程。我们知道在进行较多的I/O操作时候,比如socket server之类的可以用到多线程,那么什么时候用多进程...
  • c++多进程编程

    千次阅读 2020-04-16 10:12:52
    c++多进程编程 介绍 进程:进程是一个正在执行的程序,是向CPU申请资源的,进程之间数据相互独立,一个进程至少有一个线程。 线程:线程是进程中的单一的顺序控制流程也可以叫做最小控制单元,线程是进程中执行单元...
  • Linux多进程编程(典藏、含代码)

    千次阅读 多人点赞 2020-02-03 21:44:40
    1.2、多进程(任务)并行的实现 1.3、重要指令 1.4、父子进程和进程ID 二、多进程编程 2.1创建子进程 (fork/vfork 叉子) 2.1.1 fork 2.1.2vfork 2.2进程结束 2.2.1正常退出 2.2.2异常退出 2.3等待进程结束并...
  • 由于python内部GIL(全局解释器锁)的存在,所以python的线程实际上并不能很好的起到任务并行处理的作用,尤其是无法充分利用系统多核的优势,因此想要利用多核处理并行任务,就需要用到多进程——multiprocess。...
  • Python之多进程和多线程详解

    万次阅读 多人点赞 2018-10-16 18:02:56
    1.进程的概念 一个CPU的时候运行,轮询调度实现并发执行 CPU运行机制: 计算机程序:存储在磁盘上的可执行二进制(或其他类型)文件。 只有把它们加载到内存中,并被操作系统调用它们才会拥有其自己的生命周期。...
  • python多进程技术

    千次阅读 2019-03-08 13:10:45
    进程的概念 可执行的代码就叫程序。 正在运行着的代码+需要的一些资源就是进程。 例子:QQ没打开的时候是程序,打开了之后是进程。 fork fork()调用一次,返回两次。从返回处开始,父子进程的代码就是一样的了。 ...
  • C语言多任务,多进程,多线程

    千次阅读 2018-04-03 15:44:08
    Linux就是一种支持多任务的操作系统,它支持多进程、多线程等多任务处理和任务之间的多种通信机制。Linux下多任务机制的介绍多任务处理是指用户在同一时间内运行多个应用程序,每个应用程序被称做一个任务。Linux...
  • Python使用multiprocessing实现多进程

    万次阅读 2019-06-05 22:14:11
    Python使用multiprocessing实现多进程 进程是操作系统进行资源分配和调度的一个基本单位。 一、进程及多任务介绍 程序:程序是一个静态的概念。在一台电脑上,我们安装了很多程序,这些程序是可以运行的。比如...
  • QT多进程编程

    千次阅读 2019-11-08 17:05:24
    单个进程 使用QT中的QProcess类来启动一个外部·程序并与其进行通信。 要启动一个进程,可以使用start()函数,然后传递给该函数程序名称和运行这个程序所要使用的命令行参数。 执行完start函数,QProcess进入...
  • DPDK多进程支持

    千次阅读 2020-11-03 14:13:44
    不同类型的dpdk进程对大页内存的使用具有不同的权限,目前两种类型的进程: primary process,这些内存可以初始化并对共享内存拥有完全权限。 secondary process,不能初始化共享内存,但可以attach到预初始化的共享...
  • 试玩NodeJS多进程

    万次阅读 多人点赞 2018-04-11 16:30:11
    NodeJS的JavaScript运行在单个进程的单个线程上,一个JavaScript执行进程只能利用一个CPU核心,而如今大多数CPU均为多核CPU,为了充分利用CPU资源,Node提供了child_process和cluster模块来实现多进程以及进程管理。...
  • python类内部实现多进程

    千次阅读 2019-03-19 23:10:22
    python类内部实现多进程 工作中写一个自动化脚本, 为了提高效率, 需要在类的内部启多进程, 代码逻辑如下: # –*– coding: utf-8 –*– # @Time : 2019/3/19 21:11 # @Author : Damon_duanlei # @FileName : process...
  • python多进程 获取子进程的返回值

    千次阅读 2020-05-21 12:34:21
    1、使用进程的队列multiprocessing.Queue,put(),get()方法 子进程不需返回值,将目标结果放入队列中 在主进程中获取 get方法 """ coding:utf-8""" #coding:utf-8 import random import time import multiprocessing...
  • python--subprocess.Popen()多进程

    千次阅读 2019-09-17 21:00:49
    进程的终止 首先来看一段代码: p = subprocess.Popen(['echo','helloworl.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(p.poll()) print('Exit code:', ...
  • 关于Django多进程多线程详解

    万次阅读 2019-05-08 15:01:34
    文章目录Django 线程#1 环境#2 Django原生单线程#3 Django server 默认线程#3.1 测试#4 使用 uWSGI 服务器启动 django#4.1 启动 uWSGI#4.2 给uWSGI加进程数#5 总结关于django的线程问题 Django 线程 #1 环境...
  • 在windows环境下的IDE/Pycharm,无法运行多进程的问题 解决: 在CMD下运行既可. 原因: 待解决 cookies: 两个方法一起进行的办法: def test(): pritn("1") def test2(): print("2") if __name__...
  • 多任务中 多线程和多进程的区别

    万次阅读 多人点赞 2019-04-26 14:22:22
    任务中 线程和线程的区别
  • python的多线程不是真正的多线程,所以使用多进程来实现高并发,比如训练模型读取数据时,但是kill只会杀死相应的进程ID,由于真实环境下子进程太多,一个一个去kill太麻烦,下面实现了只需要杀死主进程id即可同时使...
  • windows下 c++多进程

    千次阅读 2019-05-28 16:08:40
    转自:... 主进程: #include<iostream> #include<windows.h> int main(int argc, char*argv[]) { STARTUPINFO si = { sizeof(STARTUPINFO) };/...
  • 在之前的文章中对多进程的一些基础概念,进程的生命周期和python进程操作的模块做了说明,本篇文章直接上代码,结束python中创建多进程的一些方法。 os.fork()(Linux) fork()函数,只在Linux系统下存在。而且它...
  • 小编我今天就来尝试下用一文总结下Python多进程和多线程的概念和区别, 并详细介绍如何使用python的multiprocess和threading模块进行多线程和多进程编程。   重要知识点 - 什么是进程(proces...
  • Android 多进程的基础使用及优缺点

    千次阅读 2017-04-08 19:20:58
    前言说起进程,不得不说说进程和线程之前的关系。在操作系统角度描述,线程是CPU调度的最小...我们可以使用多进程分担主进程压力以免因资源消耗过大被crash掉,另外多进程相互监听可以唤醒,使应用程序长期驻守后...
  • 很想写点关于多进程和多线程的东西,我确实很爱他们。但是每每想动手写点关于他们的东西,却总是求全心理作祟,始终动不了手。 今天终于下了决心,写点东西,以后可以再修修补补也无妨。一.为何需要多进程(或者多...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,837,723
精华内容 735,089
关键字:

多进程