zoukankan      html  css  js  c++  java
  • Python 多进程和进程池

    一,前言

    • 进程:是程序,资源集合,进程控制块组成,是最小的资源单位
      • 特点:就对Python而言,可以实现真正的并行效果
      • 缺点:进程切换很容易消耗cpu资源,进程之间的通信相对线程来说比较麻烦  
    • 线程:是进程中最小的执行单位。
      • 特点无法利用多核,无法实现真正意义上是并行效果。
      • 优点:对于IO密集型的操作可以很好利用IO阻塞的时间

    二,多进程

      2.1 multiprocessing模块介绍

      在上一节多线程中讲到,由于GIL的原因,多线程无法利用多核优势,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。Python提供了非常好用的多进程包multiprocessing。multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

      2.2 process类介绍

    类的实例化(创建进程)
    Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)
    
    强调:
    1. 需要使用关键字的方式来指定参数
    2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
    
    参数介绍
    group参数未使用,值始终为None
    
    target表示调用对象,即子进程要执行的任务
    
    args表示调用对象的位置参数元组,args=(1,2,'hexin',)
    
    kwargs表示调用对象的字典,kwargs={'name':'hexin','age':18}
    
    name为子进程的名称
    
    # process方法
    p.start():启动进程,并调用该子进程中的p.run() 
    p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法  
    
    p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁
    p.is_alive():如果p仍然运行,返回True
    
    p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程


    # process 属性
    p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
    
    p.name:进程的名称
    
    p.pid:进程的pid
    
    p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
    
    p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)
    
    

      2.3 多进程创建

      创建形式一:普通形式

    import time
    import random
    from multiprocessing import Process
    
    
    def fun(name):
        print('%s begin' %name)
        time.sleep(random.randrange(1, 3))
        print('%s end' % name)
    
    
    if __name__ == '__main__':
        p1 = Process(target=fun, args=('w',))
        p2 = Process(target=fun,args=('a',))
        p3 = Process(target=fun,args=('l',))
        p4 = Process(target=fun,args=('l',))
    
        p1.start()
        p2.start()
        p3.start()
        p4.start()
        print('主线程')

      输出结果:

    主线程
    w begin
    a begin
    l begin
    l begin
    a end
    l end
    w end
    l end

      创建方式二:继承方式

    import time
    import random
    from multiprocessing import Process
    
    
    class Sleep(Process):
        def __init__(self,name):
            super().__init__()
            self.name = name
    
        def run(self):
            print('%s sleep begin' % self.name)
            time.sleep(random.randrange(1,5))
            print('%s  end' % self.name)
    
    
    if __name__ == '__main__':
        for i in ['a', 'b', 'c']:
            Sleep(i).start()
        print("main")

      输出:

    main
    b sleep begin
    a sleep begin
    c sleep begin
    c  end
    b  end
    a  end

      2.4 进程同步

      进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的。

      共享同一打印终端,发现会有多行内容打印到一行的现象(多个进程共享并抢占同一个打印终端,乱了)

      既然可以用文件共享数据,那么进程间通信用文件作为数据传输介质就可以了啊,可以,但是有问题:1.效率 2.需要自己加锁处理

      加锁的目的是为了保证多个进程修改同一块数据时,同一时间只能有一个修改,即串行的修改,没错,速度是慢了,牺牲了速度而保证了数据安全。

      文件当做数据库,模拟抢票(Lock互斥锁)

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    #文件db的内容为:{"count":2}
    #注意一定要用双引号,不然json无法识别
    from multiprocessing import Process,Lock
    import json
    import time
    import random
    import os
    
    def work(filename,lock): #买票
        # lock.acquire()
        with lock:
            with open(filename,encoding='utf-8') as f:
                dic=json.loads(f.read())
                # print('剩余票数: %s' % dic['count'])
            if dic['count'] > 0:
                dic['count']-=1
                time.sleep(random.randint(1,3)) #模拟网络延迟
                with open(filename,'w',encoding='utf-8') as f:
                    f.write(json.dumps(dic))
                print('%s 购票成功' %os.getpid())
            else:
                print('%s 购票失败' %os.getpid())
        # lock.release()
    
    if __name__ == '__main__':
        lock=Lock()
        p_l=[]
        for i in range(10):
            p=Process(target=work,args=('db',lock))
            p_l.append(p)
            p.start()
        for p in p_l:
            p.join()
    
        print('主线程')

      输出:

    购票成功
    购票成功
    购票失败
    购票失败
    购票失败
    购票失败
    购票失败
    购票失败
    购票失败
    购票失败
    主线程

    三,进程间通信

      进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的。

      3.1 进程间通信(IPC)方式一:队列(推荐使用)

      队列方式一:Queue()

      队列先进先出,栈后进先出,创建队列的类(底层就是以管道和锁定的方式实现):

    # 实例创建
    Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
    
    # 参数说明
    maxsize是队列中允许最大项数,省略则无大小限制。
    
    # 属性介绍
    
    q.put方法用以插入数据到队列中
    put方法还有两个可选参数:blocked和timeout。
    如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。
    如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
    
    q.get方法可以从队列读取并且删除一个元素。
    get方法有两个可选参数:blocked和timeout。
    如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。
    如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
     
    q.get_nowait():同q.get(False)
    q.put_nowait():同q.put(False)
    
    q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
    q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
    q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样

      实例:基于队列实现生产者和消费者模型

    from multiprocessing import Process,Queue
    import time,random,os
    
    
    def consumer(q):
        while True:
            time.sleep(random.randint(1,3))
            res=q.get()
            if res is None:break
            print('33[45m消费者拿到了:%s33[0m' %res)
    
    def producer(seq,q):
        for item in seq:
            time.sleep(random.randint(1,3))
            print('33[46m生产者生产了:%s33[0m' %item)
    
            q.put(item)
    
    if __name__ == '__main__':
        q=Queue()
    
        c=Process(target=consumer,args=(q,))
        c.start()
    
        producer(('包子%s' %i for i in range(5)),q)
        q.put(None)
        c.join()
        print('主线程')

      输出:

    生产者生产了:包子0
    消费者拿到了:包子0
    生产者生产了:包子1
    消费者拿到了:包子1
    生产者生产了:包子2
    消费者拿到了:包子2
    生产者生产了:包子3
    消费者拿到了:包子3
    生产者生产了:包子4
    消费者拿到了:包子4
    主线程

      队列方式二:JoinableQueue()

    JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
    
    maxsize是队列中允许最大项数,省略则无大小限制。
    
    JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:
    
        q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常
        q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止

      生产者消费者模型:

    from multiprocessing import Process,JoinableQueue
    import time,random
    def consumer(q):
        while True:
            # time.sleep(random.randint(1,2))
            res=q.get()
            print('消费者拿到了 %s' %res)
            q.task_done()
    
    
    def producer(seq,q):
        for item in seq:
            # time.sleep(random.randrange(1,2))
            q.put(item)
            print('生产者做好了 %s' %item)
        q.join()
    
    if __name__ == '__main__':
        q=JoinableQueue()
        seq=('包子%s' %i for i in range(5))
    
        p=Process(target=consumer,args=(q,))
        p.daemon=True #设置为守护进程,在主线程停止时p也停止,但是不用担心,producer内调用q.join保证了consumer已经处理完队列中的所有元素
        p.start()
    
        producer(seq,q)
    
        print('主线程')

      3.2 进程间通信(IPC)方式二:管道

      

    # 创建实例
    Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道
    
    # 参数介绍
    dumplex:默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
    
    # 方法介绍
    conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
    conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
    
    conn1.close():关闭连接。如果conn1被垃圾回收,将自动调用此方法
    conn1.fileno():返回连接使用的整数文件描述符
    conn1.poll([timeout]):如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。
     
    conn1.recv_bytes([maxlength]):接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,
    并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。 conn.send_bytes(buffer [, offset [, size]]):通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收 conn1.recv_bytes_into(buffer [, offset]):接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。
    offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。

      生产者消费者实例:

    from multiprocessing import Process,Pipe
    
    import time,os
    def consumer(p,name):
        left,right=p
        left.close()
        while True:
            try:
                baozi=right.recv()
                print('%s 收到包子:%s' %(name,baozi))
            except EOFError:
                right.close()
                break
    def producer(seq,p):
        left,right=p
        right.close()
        for i in seq:
            left.send(i)
            # time.sleep(1)
        else:
            left.close()
    if __name__ == '__main__':
        left,right=Pipe()
    
        c1=Process(target=consumer,args=((left,right),'c1'))
        c1.start()
    
    
        seq=(i for i in range(10))
        producer(seq,(left,right))
    
        right.close()
        left.close()
    
        c1.join()
        print('主进程')

      注意:生产者和消费者都没有使用管道的某个端点,就应该将其关闭,如在生产者中关闭管道的右端,在消费者中关闭管道的左端。如果忘记执行这些步骤,程序可能再消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生产EOFError异常。因此在生产者中关闭管道不会有任何效果,付费消费者中也关闭了相同的管道端点。

      管道可以用于双向通信,利用通常在客户端/服务器中使用的请求/响应模型或远程过程调用,就可以使用管道编写与进程交互的程序,如下

    from multiprocessing import Process,Pipe
    
    import time,os
    def adder(p,name):
        server,client=p
        client.close()
        while True:
            try:
                x,y=server.recv()
            except EOFError:
                server.close()
                break
            res=x+y
            server.send(res)
        print('server done')
    if __name__ == '__main__':
        server,client=Pipe()
    
        c1=Process(target=adder,args=((server,client),'c1'))
        c1.start()
    
        server.close()
    
        client.send((10,20))
        print(client.recv())
        client.close()
    
        c1.join()
        print('主进程')

      输出:

    30
    server done
    主进程

    四,进程池

      4.1 进程池介绍

      开多进程的目的是为了并发,如果有多核,通常有几个核就开几个进程,进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行),但很明显需要并发执行的任务要远大于核数,这时我们就可以通过维护一个进程池来控制进程数目,比如httpd的进程模式,规定最小进程数和最大进程数。 

      当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,十几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,此时可以发挥进程池的功效。而且对于远程过程调用的高级应用程序而言,应该使用进程池,Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。在利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。

      4.2 进程池使用

         4.2.1进程池方式一:

         同步调用:

    from multiprocessing import Pool
    import os,time
     
    def work(n):
        print('{} run'.format(os.getpid()))
        time.sleep(1)
        return n ** 2   # ret
     
    if __name__ == '__main__':
        p = Pool(3) # 创建3个进程
        res_1 = []
        for i in range(20):
            res = p.apply(work,args=(i,))
            '''同步调用,直到本次任务执行完毕拿到res,等待任务work执行的过程中可能有阻塞也可能没有阻塞,
            但不管该任务是否存在阻塞,同步调用都会在原地等着,只是等的过程中若是任务发生了阻塞就会被夺走cpu的执行权限;
            个人理解:程序判断两个子程序执行的间隔时间,过长则判断存在阻塞,届时夺走上次进程ID的使用权限,从进程池分配新的进程ID'''
            res_1.append(res)
        print(res_1)

        异步调用:

    from multiprocessing import Pool
    import os,time
    def work(n):
        print('%s run' %os.getpid())
        time.sleep(3)
        return n**2
     
    if __name__ == '__main__':
        p=Pool(3) #进程池中从无到有创建三个进程,以后一直是这三个进程在执行任务
        res_l=[]
        for i in range(10):
            print(i)  # for循环会提前运行完毕,进程池内的任务还未执行。
            res=p.apply_async(work,args=(i,)) #同步运行,阻塞、直到本次任务执行完毕拿到res
            res_l.append(res)   #将调用apply_async方法,得到返回进程内存地址结果
     
        #异步apply_async用法:如果使用异步提交的任务,主进程需要使用join,等待进程池内任务都处理完,然后可以用get收集结果,
        # 否则,主进程结束,进程池可能还没来得及执行,也就跟着一起结束了
        p.close()
        p.join()
        for res in res_l:
            print(res.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get

        apply_sync和apply方法

    # apply_async方法
    from multiprocessing import Process,Pool
    import time
     
    def func(msg):
        print( "msg:", msg)
        time.sleep(1)
        return msg
     
    if __name__ == "__main__":
        pool = Pool(processes = 3)
        res_l=[]
        for i in range(10):
            print(i)
            msg = "hello %d" %(i)
            res=pool.apply_async(func, (msg, ))   #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
            res_l.append(res)   # 将apply_async方法得到的内存地址结果加入列表
        print("==============================>") #没有后面的join,或get,则程序整体结束,进程池中的任务还没来得及全部执行完
        # 也都跟着主进程一起结束了
     
        pool.close() #关闭进程池,防止进一步操作。如果所有操作持续挂起,它们将在工作进程终止前完成
        pool.join()   #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
     
        print(res_l) #看到的是<multiprocessing.pool.ApplyResult object at 0x10357c4e0>对象组成的列表,而非最终的结果,但这一步
        # 是在join后执行的,证明结果已经计算完毕,剩下的事情就是调用每个对象下的get方法去获取结果
        for i in res_l:
            print(i.get()) #使用get来获取apply_aync的结果,如果是apply,则没有get方法,因为apply是同步执行,立刻获取结果,也根本无需get
     
     
    #apply方法
    from multiprocessing import Process,Pool
    import time
     
    def func(msg):
        print( "msg:", msg)
        time.sleep(0.1)
        return msg
     
    if __name__ == "__main__":
        pool = Pool(processes = 3)
        res_l=[]
        for i in range(10):
            msg = "hello %d" %(i)
            res=pool.apply(func, (msg, ))   #维持执行的进程总数为processes,当一个进程执行完毕后会添加新的进程进去
            res_l.append(res) #同步执行,即执行完一个拿到结果,再去执行另外一个
        print("==============================>")
        pool.close()
        pool.join()   #调用join之前,先调用close函数,否则会出错。执行完close后不会有新的进程加入到pool,join函数等待所有子进程结束
     
        print(res_l) #看到的就是最终的结果组成的列表
        for i in res_l: #apply是同步的,所以直接得到结果,没有get()方法
            print(i)

        map方法

    #map
    import os,time
    from multiprocessing import Pool
     
    def func(i):
        time.sleep(1)
        print('子进程{}'.format(os.getpid()))
        return i
     
    if __name__ == '__main__':
        p = Pool(5)
        ret = p.map(func,range(10)) # func(next(range(10)))
        print(ret)

       4.2.1 concurrent.futures 模块实现‘池’

        同步调用:

    # #同步调用:提交/调用一个任务,然后就在原地等着,等到该任务执行完毕拿到结果,再执行下一行代码
    from concurrent.futures import ProcessPoolExecutor
    import time
    
    def save_test(name,n):
        n = n+1
        time.sleep(n)
        print("{}的名字次数为{}".format(name,n))
        return n
    
    
    if __name__ == '__main__':
        start = time.time()
        ex = ProcessPoolExecutor(max_workers=3)
        lista = ["Tom","Jerry","XiaoHua","Ming"]
    
        for i,j in enumerate(lista):
            #存在两个以上的参数时,直接用逗号隔开,不需要用括号
            task = ex.submit(save_test,j,i).result()
            print(task)
        #ex.shutdown(wait=True)是进程池内部的进程都执行完毕,才会关闭,然后执行后续代码
        ex.shutdown(wait=True)
        print("主进程直接运行")
        stop = time.time()
        print(stop-start)

        异步调用:

    #异步调用: 提交/调用一个任务,不在原地等着,直接执行下一行代码
    from concurrent.futures import ProcessPoolExecutor
    import time
    
    def save_test(name,n):
        n = n+1
        time.sleep(n)
        print("{}的名字次数为{}".format(name,n))
        return n
    
    
    if __name__ == '__main__':
        obj = list()
        start = time.time()
        ex = ProcessPoolExecutor(max_workers=3)
        lista = ["letme","Mlxg","XiaoHu","Ming"]
    
        for i,j in enumerate(lista):
            #存在两个以上的参数时,直接用逗号隔开,不需要用括号
            task = ex.submit(save_test,j,i)
            obj.append(task)
        #ex.shutdown(wait=True)是进程池内部的进程都执行完毕,才会关闭,然后执行后续代码
        ex.shutdown(wait=True)
        print("主进程直接运行")
        for i in obj:
            print(i.result())
        stop = time.time()
        print(stop-start)

        map方法使用

    from concurrent.futures import ProcessPoolExecutor
    
    import urllib.request
    URLS = ['http://www.163.com', 'https://www.baidu.com/', 'https://github.com/']
    
    
    def load_url(url):
        with urllib.request.urlopen(url, timeout=60) as conn:
            print('%r page is %d bytes' % (url, len(conn.read())))
    
    
    if __name__ == '__main__':
        executor = ProcessPoolExecutor(max_workers=3)
        executor.map(load_url,URLS)
        print('主进程')

    五,总结

      用futures的写法上更简洁一些,concurrent.futures的性能并没有更好,只是让编码变得更简单。考虑并发编程的时候,任何简化都是好事。从长远来看,concurrent.futures编写的代码更容易维护。
      使用map时,future是逐个迭代提交,multiprocessing.Pool是批量提交jobs,因此对于大批量jobs的处理,multiprocessing.Pool效率会更高一些。对于需要长时间运行的作业,用future更佳,future提供了更多的功能(callback, check status, cancel)。
      concurrent.futures.ProcessPoolExecutor是对multiprocessing的封装,在运行时需导入__main__,不能直接在交互窗口工作。

  • 相关阅读:
    汉字编码问题
    C语言创建UTF8编码文本文件
    Know more about shared pool subpool
    SCN Headroom与时光倒流到1988年的Oracle数据库
    Know more about Enqueue Deadlock Detection
    11g新特性:RDBMS Component TRACE
    了解你所不知道的SMON功能(十一):OFFLINE UNDO SEGMENT
    了解11g OCM
    Bulk Collect/FORALL的性能测试
    【推荐】DBA必须了解的11g中的一些变化
  • 原文地址:https://www.cnblogs.com/tashanzhishi/p/10775657.html
Copyright © 2011-2022 走看看