python3 进程(multiprocessing)从入门到提高--详解

0.背景知识:

1.僵尸进程(有害)

任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。
如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,
但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。

2.孤儿进程(无害)

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,
内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面
处理它的一切善后工作。因此孤儿进程并不会有什么危害。

3.总结

严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。

1.开启进程的两种方式

1.简单开启

# 方式一:使用函数开启进程
from multiprocessing import Process
import time


def task(x):
    print('%s is running' % x)
    time.sleep(1)
    print('%s is done' % x)


if __name__ == '__main__':
    # p1 = Process(target=task, args=('子进程',))    #实例化子进程
    p1 = Process(target=task, kwargs={'x': '子进程'})
    p1.start()  # 向操作系统申请资源(内存空间,子进程pid号),然后开始执行task任务,本动作不影响主进程,主进程则会继续执行。
    print('这是主进程...')

使用函数开启进程

2.类方式开启

from multiprocessing import Process
import time

class MyProcess(Process):

    def __init__(self,x):
        super(MyProcess,self).__init__()
        self.x = x

    def run(self):
        print('%s is running'%self.x)
        time.sleep(1)
        print('%s is done'%self.x)

if __name__ == '__main__':
    p1 = MyProcess('子进程')
    p1.start()
    print('这是主进程....')

使用类的方式开启进程

2.进程间是物理隔离的,不共享全局变量


from multiprocessing import Process
import time

x = 100


def task():
    global x
    x += 1
    print('子进程中执行x所得的值为------%s' % x)
    time.sleep(1)


if __name__ == '__main__':
    p1 = Process(target=task)
    p1.start()
    p1.join()
    print('父进程中执行x所得的值为------%s' % x)

# 子进程中执行x所得的值为------101
# 父进程中执行x所得的值为------100

子进程是父进程的复制品,在内存中会把父进程的代码及内存分配情况拷贝一份生成子进程的运行空间,这样子进程与父进程的所有代码都一样,两个进程之间的运行时独立的,互不影响

3.进程中使用join()函数

  • 如果子进程不使用join,那么主进程就不会等待子进程,单独运行直到结束。
  • 子进程调用join时,主进程会被阻塞。当子进程结束后,主进程才能继续执行。
  • join 函数的作用 功能是为了实现进程同步的(可以理解为子进程与主进程是同步的,一个完成了,才能完成另一个的意思)
  • 而它具体 是阻塞当前进程也就是主进程直到调用join函数的进程执行完成后继续执行当前进程。

1.子进程中不使用join()

import os
from multiprocessing import Process


def run_proc(name):
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    print("Parent process %s" % (os.getpid()))
    for i in range(5):
        p = Process(target=run_proc, args=(str(i),))
        p.start()
    print("Finished")

Parent process 2460
Child process 0 (2461)
Child process 1 (2462)
Finished
Child process 2 (2463)
Child process 3 (2464)
Child process 4 (2465)

通过如上的结果发现:

  • 主进程都结束了,但是子进程还没有结束,直到子进程运行完成。
  • 没有join,主进程不会等待任何子进程,独立运行,不受任何其他子进程影响。

2.子进程中使用join()

import os
import time
from multiprocessing import Process


def pro_func01(name):
    print("Child process %s (%s)" % (name, os.getpid()))


def pro_func02(name):
    for i in range(5):
        time.sleep(1)
        print("一共睡眠了 %s 秒种" % i)
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    print("Parent process %s" % (os.getpid()))
    p01 = Process(target=pro_func01, args=(str(1),))
    p02 = Process(target=pro_func02, args=(str(2),))
    p01.start()
    p02.start()
    p02.join()
    print("Finished")

Parent process 2608
Child process 1 (2609)
一共睡眠了 0 秒种
一共睡眠了 1 秒种
一共睡眠了 2 秒种
一共睡眠了 3 秒种
一共睡眠了 4 秒种
Child process 2 (2610)
Finished

通过以上发现,因为p2进程使用了join函数,所以主进程等待这个子程序运行结束之后才去执行主程序的最后一个打印工作。

3.多个子进程在for循环中错误使用join函数

import os
from multiprocessing import Process
from time import sleep
import time


def run_proc(name):
    sleep(1)
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    start_time = time.time()
    print("Parent process %s" % (os.getpid()))
    for i in range(5):
        p = Process(target=run_proc, args=(str(i),))
        p.start()
        p.join()
    end_time = time.time()
    print("Finished", end_time - start_time)

Parent process 2692
Child process 0 (2693)
Child process 1 (2695)
Child process 2 (2696)
Child process 3 (2701)
Child process 4 (2702)
Finished 5.041041851043701

以上是结果可以看到:

  • 在主进程中首先进入for循环启动第一个子进程然后由于调用join函数会阻塞主进程,直到第一个子进程执行完成后开始取消阻塞。
  • 继续执行主进程,开始开启第二个子进程,然后由于第二个子进程也调用了join,再阻塞主进程一直到循环结束。
  • 最终循环下来,直到所有子进程,都一个一个执行完之后,才开始执行主进程。
  • 这种方式可以实现效果,但是效率太低,既然是一个一个进程执行的,和我们使用一个进程for循环是一样的效果,没有体现多核CPU并发异步执行任务的效果。

4.多个子进程在for循环中使用join函数(改进)

import os
from multiprocessing import Process
from time import sleep
import time


def run_proc(name):
    sleep(1)
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    start_time = time.time()
    print("Parent process %s" % (os.getpid()))
    p_list = list()
    for i in range(5):
        p = Process(target=run_proc, args=(str(i),))
        p_list.append(p)
        p.start()

    for m in p_list:
        m.join()
    end_time = time.time()
    print("Finished", end_time - start_time)



Parent process 2745
Child process 0 (2746)
Child process 1 (2747)
Child process 2 (2748)
Child process 3 (2749)
Child process 4 (2750)
Finished 1.013887882232666

通过如上的改进,我们先启动所有子进程,所有任务都启动完成之后,再使用join()这样可以实现多线程的异步执行(并发的概念)。效率也提高很多

4.daemon守护进程的作用

1.不设置守护进程效果

import time
from multiprocessing import Process


def func():
    print("子进程开始.")
    time.sleep(2)
    print("子进程结束.")


if __name__ == '__main__':
    p = Process(target=func)
    p.start()
    print("主进程结束.")
主进程结束.
子进程开始.
子进程结束.

不设置守护进程的时候,虽然主进程没有等待子进程,直接先结束了。但是,前面我们提到过init进程的作用,它会接收孤儿进程,让孤儿进程继续执行下去。

2.设置守护进程效果

import time
from multiprocessing import Process


def func():
    print("子进程开始.")
    time.sleep(2)
    print("子进程结束.")


if __name__ == '__main__':
    p = Process(target=func)
    p.daemon = True
    p.start()
    print("主进程结束.")
主进程结束.

通过如上的结果:

  • 设置了守护进程,主进程还是没有等待子进程,直接结束了。但是,主进程结束了,子进程也跟着结束了。也就说明,设置守护进程之后,init进程,就不会去接收这个孤儿进程了。这个是进程是否被设置为守护进程的最大区别。
  • daemon一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行

主进程创建守护进程
   其一:守护进程会在主进程代码执行结束后就终止
   其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

5.进程间通讯队列(Queue,JoinableQueue)

1.使用Queue通讯

多进程里面使用的队列(Queue)是multiprocessing模块里面的Queue,和queue.Queue的队列模块不一样

import time
from multiprocessing import Process
from multiprocessing import Queue


def producer(food, q, name):
    for i in range(3):
        res = '%s%s' % (food, i)
        time.sleep(2)
        q.put(res)
        print('%s 生产了%s' % (name, res))
    q.put(name)


def consumer(q, name):
    while True:
        res = q.get()
        if res in ("worker_01", "worker_02", "worker_03"):
            print(res, "-------???-------")
            break
        time.sleep(3)
        print('----------%s消费了%s' % (name, res))


if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=producer, args=('包子', q, 'worker_01'))
    p2 = Process(target=producer, args=('水饺', q, 'worker_02'))
    p3 = Process(target=producer, args=('馒头', q, 'worker_03'))

    c1 = Process(target=consumer, args=(q, 'Consumer_01'))
    c2 = Process(target=consumer, args=(q, 'Consumer_02'))

    p1.start()
    p2.start()
    p3.start()

    c1.start()
    c2.start()

    p1.join()
    p2.join()
    p3.join()
    print('主...')

worker_01 生产了包子0
worker_02 生产了水饺0
worker_03 生产了馒头0
worker_03 生产了馒头1
worker_02 生产了水饺1
worker_01 生产了包子1
----------Consumer_01消费了包子0
----------Consumer_02消费了水饺0
worker_03 生产了馒头2
worker_01 生产了包子2
worker_02 生产了水饺2...
----------Consumer_01消费了馒头0
----------Consumer_02消费了包子1
----------Consumer_01消费了馒头1
----------Consumer_02消费了水饺1
----------Consumer_01消费了水饺2
----------Consumer_02消费了包子2
worker_03 -------???-------
----------Consumer_01消费了馒头2
worker_02 -------???-------

通过如上的结果:

  • 我们通过一种将队列里面input()固定的参数的形式,如果遇到这个参数,就break就可以停止从队列里面get()信息,也避免因队列为空的时候,再获取信息的报错。
  • 但这种方式容易有个弊端,通过如上的结果是看不出的(生产9个产品,消费了9个)。但如果如上的代码只有一个消费者,得到的结果如下:
worker_01 生产了包子0
worker_03 生产了馒头0
worker_02 生产了水饺0
worker_01 生产了包子1
worker_03 生产了馒头1
worker_02 生产了水饺1
----------Consumer_01消费了包子0
worker_01 生产了包子2
worker_03 生产了馒头2
worker_02 生产了水饺2...
----------Consumer_01消费了水饺0
----------Consumer_01消费了馒头0
----------Consumer_01消费了包子1
----------Consumer_01消费了馒头1
----------Consumer_01消费了水饺1
----------Consumer_01消费了水饺2
----------Consumer_01消费了包子2
worker_02 -------???-------

如上结果,生产了9个,但是只消费了8个就结束了,这个不是我们想要的结果,
原因是我们将产品input到队列的时候,产品和特殊字符的顺序可能不一致,可能有多种多样的结果,例如如下几种情况:

order_product = ["包子0", "水饺0", '馒头0', '包子1', '水饺1', '馒头1', '包子2', 'worker_01', '水饺2', 'worker_02', '馒头2', 'worker_03']

如上这种情况,如果只有2个消费者,那么只能消费8个产品,如果一个消费者可以消费7个产品,因为遇到特殊字符(worker_01或worker_02等就直接结束消费了),所以使用Queue来进程间的通讯,只适合非常简答的场景。

2.使用JoinableQueue队列

(1)消费者不需要判断从队列里拿到某个特定字符(None或其他),再退出执行消费者函数了。
(2)消费者每次从队列里面q.get()一个数据,处理过后就使用队列.task_done()
(3)生产者for循环生产完所有产品,需要q.join()阻塞一下,对这个队列进行阻塞。意思是只有当队列里面的所有内容都取完了,才会调用主进程,继续执行下去
(4)启动一个生产者,启动一个消费者,并且这个消费者做成守护进程,然后生产者需要p.join()阻塞一下。


import time
import random
from multiprocessing import JoinableQueue
from multiprocessing import Process


def producer(food, q, name):
    for i in range(3):
        res = '%s%s' % (food, i)
        time.sleep(random.randint(1, 3))
        q.put(res)
        print('%s 生产了%s' % (name, res))


def consumer(q, name):
    while True:
        res = q.get()
        time.sleep(random.randint(2, 4))
        print('----------%s消费了%s' % (name, res))
        q.task_done()


if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer, args=('包子', q, 'worker_01'))
    p2 = Process(target=producer, args=('水饺', q, 'worker_02'))
    p3 = Process(target=producer, args=('馒头', q, 'worker_03'))

    c1 = Process(target=consumer, args=(q, 'Consumer_01'))
    c2 = Process(target=consumer, args=(q, 'Consumer_02'))

    p1.start()
    p2.start()
    p3.start()
    c1.daemon = True
    c2.daemon = True
    c1.start()
    c2.start()

    p1.join()
    p2.join()
    p3.join()

    q.join()
    print('主...')


worker_02 生产了水饺0
worker_01 生产了包子0
worker_03 生产了馒头0
----------Consumer_01消费了水饺0
worker_01 生产了包子1
worker_02 生产了水饺1
worker_03 生产了馒头1
----------Consumer_02消费了包子0
worker_02 生产了水饺2
worker_01 生产了包子2
----------Consumer_02消费了包子1
----------Consumer_01消费了馒头0
worker_03 生产了馒头2
----------Consumer_02消费了水饺1
----------Consumer_01消费了馒头1
----------Consumer_02消费了水饺2
----------Consumer_02消费了馒头2
----------Consumer_01消费了包子2...

通过如上发现:
1.q.join()阻塞了队列,只有队列的信息都取完之后,才能调用主进程结束
2.q.task_done()的作用是,每次从队列里面取出内容之后,使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常。(个人理解,这个函数的作用就是告诉主进程,我又取出来一个函数,直到主进程发现队列里面没有了任何信息,此时主进程才会执行,否者主进程就一直等待子进程的执行)
3.如果如上代码不使用q.task_done(),那最终生产和消费都正常,但是主进程一直无法结束,一直在子进程的死循环中运行。

6.进程池

1.使用两种不同的方式启动进程池

# for循环方式启动
from multiprocessing import Process
import time


def task(i):
    i += 1


if __name__ == '__main__':
    start = time.time()

    p_l = []

    for i in range(200):
        p = Process(target=task, args=(i,))
        p_l.append(p)
        p.start()

    for j in p_l:
        j.join()

    print(time.time() - start)

# 计算的时间为:0.20714521408081055

from multiprocessing import Pool
import time


def task(i):
    i += 1


if __name__ == '__main__':
    '''
    分配进程个数时,推荐建议使用cpu核数+1
    '''

    start = time.time()
    p = Pool(5)  # 相当于实例化了 5 个进程
    p.map(task, range(200))  # 相当于 p.start()  ,天生异步,每次执行5个进程

    p.close()  # 为了防止继续往里面提交任务,保护进程池,关闭掉进程池所接收的任务通道
    p.join()  # 等待子进程执行完毕

    print(time.time() - start)  # 计算开启进程所消耗的总时间

# 计算最终的时间为: 0.11593031

最终发现,使用第二种方式,直接创建进程池,然后把任务需要执行的数据,放在一个列表里面传递给函数即可。

2.进程池使用apply启动进程(同步)

from multiprocessing import Pool
import time
import random


def task(i):
    i += 1
    print(i)
    time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    p = Pool(5)
    for i in range(20):
        p.apply(task, args=(i,))  # apply天生同步


虽然启动的进程池,但是所有进程还是一个一个执行的,和单进程没啥区别,不推荐使用。

3.进程池使用apply_async(异步,但错误使用)

from multiprocessing import Pool
import time
import random


def task(i):
    i += 1
    print(i)
    time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    p = Pool(5)
    for i in range(20):
        res = p.apply_async(task, args=(i,))  # apply_async天生异步
        res.get()  # 如果在for循环中,使用res.get(),则整个程序又变成了同步的状态

4.进程池使用apply_async(异步,优化之后的使用)


from multiprocessing import Pool
import time
import random


def task(i):
    i += 1
    print(i)
    time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    p = Pool(5)
    res_li = []

    for i in range(20):
        res = p.apply_async(task, args=(i,))  # apply_async天生异步
        res_li.append(res)  # 将所有 apply_async返回的结果 都添加到列表中再get(),则程序又恢复异步

    for res in res_li:
        res.get()

每次启动5个进程,去执行task任务,分4个批次执行完成。基本实现了进程的并发

文章来源:
https://www.cnblogs.com/lich1x/p/10235610.html
https://blog.csdn.net/weixin_43751285/article/details/92837030
https://www.cnblogs.com/gengyi/p/8564052.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章