python的多进程和多线程

python的多进程和多线程

线程和进程

​ 1.进程层次要高于线程。一个进程里面包含了一个或者多个线程。进程是计算机一个抽象任务的统称也是表示为此任务分配的内存空间(PID);线程是计算机调用进程资源的最小单位,每个进程至少有一个线程。其实我们可以这么理解:进程是资源的调配,而线程是CPU的调度。

​ 2.进程单独有一块资源空间,不同的进程之间只能通过管道通信;统一进程中的线程之间可以直接通信和影响,因为它们都是共享的进程的内存空间。()

​ 3.单核CPU只能同一时间运行一个进程;但是CPU多核心单处理器的优势是将任务划分为很多块,每个核心负责处理一小块。而python不存在真正的多进程,因为它的解释器只能运行在一个进程上。

​ 4.对于计算密集型应用,应该使用多进程;(实际上这个对于多核单CPU是没有什么用的。)对于IO密集型应用,应该使用多线程。线程的创建比进程的创建开销小的多。.

​ 教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”但是现实使用过程中是进程和线程联合使用的。

​ 多线程适合于使用IO密集型的任务;多进程适合于计算密集型任务,(python解释器不会协调内核的计算。多进程中的进程是由操作系统来协调的,放在哪个CPU上跑,计算占比等等,要想控制这个,需要控制计算机。)


名称解释

​ 上下文切换,有时也称做进程切换或任务切换,是指CPU 从一个进程或线程切换到另一个进程或线程。或线程切换到另一个进程或线程。

​ 并行:多个任务同时运行,只有具备多个CPU才能实现并行,含有几个CPU,也就意味着在同一时刻可以执行几个任务。
​ 并发:是伪并行,即看起来是同时运行的,实际上是单个CPU在多道程序之间来回的进行切换。

​ 同步和异步的概念:

​ 这个概念属于程序执行过程的特性。一般来说程序来说都是顺序执行,上一步不执行完毕不能执行下一步。这就是同步;而不需要等待上一步的执行完毕,就开始下一步的执行就是异步。:happy:

同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。换句话来说就是调用者主动等待这个调用的结果。

异步,当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

阻塞:指的是程序未得到所需的计算资源被挂起的状态。程序在等待某个操作完成期间,自身无法继续干别的事情。则称该程序操作上是阻塞的:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输入阻塞等。阻塞是无处不在的,包括 CPU 切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。

​ **非阻塞:**非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。


出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

同步阻塞

1.老王用水壶煮水,并且*站在那里*,*不管水开没开,每隔一定时间看看水开了没*。

同步非阻塞

2.老王还是用水壶煮水,不再傻傻的站在那里看水开,*跑去寝室上网*,*但是还是会每隔一段时间过来看看水开了没有,水没有开就走人*。-

异步阻塞

3.老王这次使用高大上的响水壶来煮水,*站在那里*,*但是不会再每隔一段时间去看水开,而是等水开了,水壶会自动的通知他*。-

异步非阻塞

4.老王还是使用响水壶煮水,*跑到客厅上网去*,等着响水壶*自己把水煮熟了以后通知他*。-

老王豁然,这下感觉轻松了很多。

同步和异步

同步就是烧开水,需要自己去轮询(每隔一段时间去看看水开了没),异步就是水开了,然后水壶会通知你水已经开了,你可以回来处理这些开水了。

同步和异步是相对于操作结果来说,会不会等待结果返回。

  • **阻塞和非阻塞**

    阻塞就是说在煮水的过程中,你不可以去干其他的事情,非阻塞就是在同样的情况下,可以同时去干其他的事情。阻塞和非阻塞是相对于线程是否被阻塞。

​ 阻塞和非阻塞是指进程访问的数据如果尚未就绪,进程是否需要等待,简单说是程序对于未得到结果的反应,继续等待调用结果就是阻塞式调用,不等待调用结果,继续处理别的操作。隔断时间再来询问之前的操作是否完成。也叫做轮询就是非阻塞式调用。这是函数内部的实现区别。

​ 同步和异步关注的是获得结果的方式。区别同步就是硬是追着等待程序调用的结果,异步则是不直接结果,等程序运行完毕通知它再回来执行刚才没执行完的操作,同步一般指的是主动要求得到程序调用的结果;异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。

​ 打电话的过程就是同步,发短信的过程就是异步。

​ 子进程:为何需要使用子进程?一个http服务器需要用多个子进程来应对客户端不同的请求。有的返回图片,有的返回文字等等都是通过子进程来实现的。


如果多个任务共用同一个资源空间,那么必须在一个进程内开启多个线程。

计算密集型:计算密集型任务数应等于CPU核心数。由于计算密集型任务主要消耗CPU资源,索引代码运行效率至关重要。python这样的脚本语言效率低不适合运行计算密集型任务。

IO密集型:设计到网络,磁盘IO的任务都是IO密集型任务。这类任务的特点是CPU消耗很少,大部分时间都在等待IO操作。对于此类任务,任务越多CPU效率越高。

​ 无论是计算密集还是IO密集,任务多了效率一定上不去,因为切换任务是需要成本的。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。总之,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。任务的状态保存及再加载,这段过程就叫做上下文切换。当有几千任务在同时执行时间,CPU上下文切换都忙不过来,根本没有办法顾及任务。这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。

​ 并发和并行:并发通常指有多个任务需要同时进行,并行则是同一时刻有多个任务执行。

​ 用上课来举例就是,并发情况下是一个老师在同一时间段辅助不同的人功课。并行则是好几个老师分别同时辅助多个学生功课。简而言之就是一个人同时吃三个馒头还是三个人同时分别吃一个的情况,吃一个馒头算一个任务。

​ 是否并发可以理解成为多线程共享内存,并行为多进程独立运行?

对应的模块

python中对应的模块:

multiprocess来运行多进程。主要用于程序自身的fork()。外带queue和manage

subprocess来运行多进程。因为subprocess主要是用于和外部程序做交互的,而且它还有管道输出的功能,是进程通信的好工具。

threating运行多线程

线程的特点是公用一块内存资源;而进程则都是相对独立的。

​ 例如公用资源是一个空列表,通过线程向空列表中添加数据,那么列表会不断变大。但是通过进程添加数据则每个进程显示的都是本进程添加的数据。

内存管理机制

​ GIL 是python的全局解释器锁,同一进程中假如有多个线程运行,一个线程在运行python程序的时候会霸占python解释器(加了一把锁即GIL),使该进程内的其他线程无法运行,等该线程运行完后其他线程才能运行。如果线程运行过程中遇到耗时操作,则解释器锁解开,使其他线程运行。所以在多线程中,线程的运行仍是有先后顺序的,并不是同时进行。

​ 每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。

主要的方法

实例

多进程实例

一个主进程下存在多个子进程(a子进程,b子进程,c子进程),当(a,b,c)任意一个子进程挂掉后,不会把子进程的所有信息都回收(会回收子进程所占用的CPU,内存,打开的文件数等资源),但是(CPU占用时间,子进程的PID)等信息会被留下。--**这就属于僵尸进程**(子进程不占CPU,内存了,但是子进程的所有数据还没有被完全回收)。              

​ 所有的子进程都会经历僵尸进程这么一个过程。这种状态是为了能让父进程在任何时刻都能检测到子进程相关的信息。

​ **当子进程挂了,父进程没有挂掉,**并且父进程一直不发送回收子进程的相关信息的操作,就会产生大量的僵尸进程。大量的僵尸进程会占用PID号,导致正常的进程启动会受影响。

解决方法:当父进程挂掉之后,这种状态就会消失!父进程在挂掉之前会发起一个系统调用(waitPID),会把所有子进程的PID信息全部都回收掉,那么此时子进程的所有信息将全部被回收。所以kill父进程就可以kill僵尸进程。

进程和线程不同的地方:

运行完毕并非终止运行

1 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,

2 主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

3

​ 看下面的范例,当p的方法为apply 的时候,进程运行完一个再运行下一个所以得到的结果是从小到大的顺序排列。但是当使用apply_async的时候,进程使用多核心进行计算。得到的结果是没有顺序的。

def f(x):
    time.sleep(1)
    print (x*x)
    return x*x
if __name__ =='__main__':#进程不能共用内存
    # for i in range(10):
    #     p = Process(target=f1,args=(i,))
    #     p.start()

    p=Pool(16)
    for j in range(0,90):
        p.apply_async(f,args=(j,))
    p.close()
    p.join()
    
    
    
    #这是更加优雅的写法

    with Pool(5) as p:
        for i in range(1,20):
            res = p.apply_async(f,[i])
            res_list.append(res)
         

*注意:这里多进程的方法很有趣,args后面必须接(,)

apply_async是异步执行的,可以设置回调函数:

pool.apply_async(func``=``Foo,args``=``(i,),callback``=``Bar)

join:阻塞当前进程,直到调用join方法的那个进程执行完,再继续执行当前进程。

守护进程:

多线程(线程没有主次之分)

知识点一:
当一个进程启动之后,会默认产生一个主线程,因为线程是程序执行流的最小单元,当设置多线程时,主线程会创建多个子线程,在python中,默认情况下(其实就是setDaemon(False)),主线程执行完自己的任务以后,就退出了,此时子线程会继续执行自己的任务,直到自己的任务结束,例子见下面一。

import threading
import time

def run():
    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)


if __name__ == '__main__':

    start_time = time.time()

    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)

    for t in thread_list:
        t.start()

    print('主线程结束!' , threading.current_thread().name)
    print('一共用时:', time.time()-start_time)

知识点二:
当我们使用setDaemon(True)方法,设置子线程为守护线程时,主线程一旦执行结束,则全部线程全部被终止执行,可能出现的情况就是,子线程的任务还没有完全执行结束,就被迫停止,例子见下面二。

import threading
import time

def run():

    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)


if __name__ == '__main__':

    start_time = time.time()

    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)

    for t in thread_list:
        t.setDaemon(True)
        t.start()

    print('主线程结束了!' , threading.current_thread().name)
    print('一共用时:', time.time()-start_time)

知识点三:
此时join的作用就凸显出来了,join所完成的工作就是线程同步,即主线程任务结束之后,进入阻塞状态,一直等待其他的子线程执行结束之后,主线程再继续往下运行,例子见下面三。

import threading
import time

def run():

    time.sleep(2)
    print('当前线程的名字是: ', threading.current_thread().name)
    time.sleep(2)


if __name__ == '__main__':

    start_time = time.time()

    print('这是主线程:', threading.current_thread().name)
    thread_list = []
    for i in range(5):
        t = threading.Thread(target=run)
        thread_list.append(t)

    for t in thread_list:
        t.setDaemon(True)
        t.start()

    for t in thread_list:
        t.join()

    print('主线程结束了!' , threading.current_thread().name)
    print('一共用时:', time.time()-start_time)

知识点四:
join有一个timeout参数:

  1. 当设置守护线程时,含义是主线程对于子线程等待timeout的时间将会杀死该子线程,最后退出程序。所以说,如果有10个子线程,全部的等待时间就是每个timeout的累加和。简单的来说,就是给每个子线程一个timeout的时间,让他去执行,时间一到,不管任务有没有完成,直接杀死。
  2. 没有设置守护线程时,主线程将会等待timeout的累加和这样的一段时间,时间一到,主线程结束,但是并没有杀死子线程,子线程依然可以继续执行,直到子线程全部结束,程序退出。

知识点五:

如果子线程挂掉的话,将会导致整个进程挂掉。

总结

​ 一般默认的情况不加守护线程和阻塞线程的话,就是主线程运行完毕结束,子线程继续执行,直到子线程运行结束

​ 有时我们需要的是,子线程运行完,才继续运行主线程,这时就可以用join方法(在线程启动后面);threat.join()表示等待这个线程运行完成。

但是有时候我们需要的是,只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以用setDaemon方法(在线程启动前面)。

​ 比如在启动线程前设置thread.setDaemon(True),就是设置该线程为守护线程,表示该线程是不重要的,进程退出时不需要等待这个线程执行完成。thread.setDaemon()设置为True, 则设为true的话 则主线程执行完毕后会将子线程回收掉,设置为false,主进程执行结束时不会回收子线程。

​ 下面这个范例说明了多线程的特点:主线程先运行完毕,等待非守护线程运行完毕,非守护进程运行完毕以后主线程终止,守护线程如果现在还没有运行完毕就会被迫退出。

import threading
import time
import os

def run1():
    time.sleep(2)
    print('当前线程的名字是run1: ', threading.current_thread().name,os.getpid())

def run2():
    time.sleep(5)
    print('当前线程的名字是run2: ', threading.current_thread().name)

def run3():
    time.sleep(10)
    print('当前线程的名字是run3: ', threading.current_thread().name)
    
    
if __name__=='__main__':
    start_time = time.time()
    print('这是主线程:', threading.current_thread().name)
    t1=threading.Thread(target=run1)
    t2=threading.Thread(target=run2)
    t3=threading.Thread(target=run3)

    t3.daemon=True
    t1.start()
    t2.start()
    t3.start()
    print ('主线程运行完毕')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值