Python 多进程原理及实现


Posted in Python onDecember 21, 2020

1 进程的基本概念

什么是进程?

​ 进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

进程的生命周期:创建(New)、就绪(Runnable)、运行(Running)、阻塞(Block)、销毁(Destroy)

进程的状态(分类):(Actived)活动进程、可见进程(Visiable)、后台进程(Background)、服务进程(Service)、空进程

2 父进程和子进程​

Linux 操作系统提供了一个 fork() 函数用来创建子进程,这个函数很特殊,调用一次,返回两次,因为操作系统是将当前的进程(父进程)复制了一份(子进程),然后分别在父进程和子进程内返回。子进程永远返回0,而父进程返回子进程的 PID。我们可以通过判断返回值是不是 0 来判断当前是在父进程还是子进程中执行。

​ 在 Python 中同样提供了 fork() 函数,此函数位于 os 模块下。

# -*- coding: utf-8 -*- 
import os
import time

print("在创建子进程前: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))

pid = os.fork()
if pid == 0:
  print("子进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))
  time.sleep(5)
else:
  print("父进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))
  # pid表示回收的子进程的pid
  #pid, result = os.wait() # 回收子进程资源阻塞
  time.sleep(5)
  #print("父进程:回收的子进程pid=%d" % pid)
  #print("父进程:子进程退出时 result=%d" % result)

# 下面的内容会被打印两次,一次是在父进程中,一次是在子进程中。
# 父进程中拿到的返回值是创建的子进程的pid,大于0
print("fork创建完后: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))

2.1 父子进程如何区分?

子进程是父进程通过fork()产生出来的,pid = os.fork()

​ 通过返回值pid是否为0,判断是否为子进程,如果是0,则表示是子进程

​ 由于 fork() 是 Linux 上的概念,所以如果要跨平台,最好还是使用 subprocess 模块来创建子进程。

2.2 子进程如何回收?

python中采用os.wait()方法用来回收子进程占用的资源

pid, result = os.wait() # 回收子进程资源

阻塞,等待子进程执行完成回收

如果有子进程没有被回收的,但是父进程已经死掉了,这个子进程就是僵尸进程。

3 Python进程模块

python的进程multiprocessing模块有多种创建进程的方式,每种创建方式和进程资源的回收都不太相同,下面分别针对Process,Pool及系统自带的fork三种进程分析。

3.1 fork()

import os
pid = os.fork() # 创建一个子进程
os.wait() # 等待子进程结束释放资源
pid为0的代表子进程。

缺点: 
​ 1.兼容性差,只能在类linux系统下使用,windows系统不可使用; 
​ 2.扩展性差,当需要多条进程的时候,进程管理变得很复杂; 
​ 3.会产生“孤儿”进程和“僵尸”进程,需要手动回收资源。 
优点: 
​ 是系统自带的接近低层的创建方式,运行效率高。

3.2 Process进程

multiprocessing模块提供Process类实现新建进程

# -*- coding: utf-8 -*-
import os
from multiprocessing import Process
import time

def fun(name):
  print("2 子进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))
  print("hello " + name)


def test():
  print('ssss')


if __name__ == "__main__":
  print("1 主进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))
  ps = Process(target=fun, args=('jingsanpang', ))
  print("111 ##### ps pid: " + str(ps.pid) + ", ident:" + str(ps.ident))
  print("3 进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))
  print(ps.is_alive()) # 启动之前 is_alive为False(系统未创建)
  ps.start()
  print(ps.is_alive()) # 启动之后,is_alive为True(系统已创建)

  print("222 #### ps pid: " + str(ps.pid) + ", ident:" + str(ps.ident))
  print("4 进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))
  ps.join() # 等待子进程完成任务  类似于os.wait()
  print(ps.is_alive())
  print("5 进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))
  ps.terminate() #终断进程
  print("6 进程信息: pid=%s, ppid=%s" % (os.getpid(), os.getppid()))

特点: 

​1.注意:Process对象可以创建进程,但Process对象不是进程,其删除与否与系统资源是否被回收没有直接的关系。 
2.主进程执行完后会默认等待子进程结束后回收资源,不需要手动回收资源;join()函数用来控制子进程结束的顺序,其内部也有一个清除僵尸进程的函数,可以回收资源; 
3.Process进程创建时,子进程会将主进程的Process对象完全复制一份,这样在主进程和子进程各有一个 Process对象,但是p.start()启动的是子进程,主进程中的Process对象作为一个静态对象存在,不执行。

4.当子进程执行完毕后,会产生一个僵尸进程,其会被join函数回收,或者再有一条进程开启,start函数也会回收僵尸进程,所以不一定需要写join函数。 
5.windows系统在子进程结束后会立即自动清除子进程的Process对象,而linux系统子进程的Process对象如果没有join函数和start函数的话会在主进程结束后统一清除。

另外还可以通过继承Process对象来重写run方法创建进程

3.3 进程池POOL (多个进程)

import multiprocessing
import time

def work(msg):
  mult_proces_name = multiprocessing.current_process().name
  print('process: ' + mult_proces_name + '-' + msg)


if __name__ == "__main__":
  pool = multiprocessing.Pool(processes=5) # 创建5个进程
  for i in range(20):
    msg = "process %d" %(i)
    pool.apply_async(work, (msg, ))
  pool.close() # 关闭进程池,表示不能在往进程池中添加进程
  pool.join() # 等待进程池中的所有进程执行完毕,必须在close()之后调用
  print("Sub-process all done.")

上述代码中的pool.apply_async()是apply()函数的变体,apply_async()是apply()的并行版本,apply()是apply_async()的阻塞版本,使用apply()主进程会被阻塞直到函数执行结束,所以说是阻塞版本。apply()既是Pool的方法,也是Python内置的函数,两者等价。可以看到输出结果并不是按照代码for循环中的顺序输出的。

多个子进程并返回值

apply_async()本身就可以返回被进程调用的函数的返回值。上一个创建多个子进程的代码中,如果在函数func中返回一个值,那么pool.apply_async(func, (msg, ))的结果就是返回pool中所有进程的值的对象(注意是对象,不是值本身)。

import multiprocessing
import time

def func(msg):
  return multiprocessing.current_process().name + '-' + msg

if __name__ == "__main__":
  pool = multiprocessing.Pool(processes=4) # 创建4个进程
  results = []
  for i in range(20):
    msg = "process %d" %(i)
    results.append(pool.apply_async(func, (msg, )))
  pool.close() # 关闭进程池,表示不能再往进程池中添加进程,需要在join之前调用
  pool.join() # 等待进程池中的所有进程执行完毕
  print ("Sub-process(es) done.")

  for res in results:
    print (res.get())

与之前的输出不同,这次的输出是有序的。

​如果电脑是八核,建立8个进程,在Ubuntu下输入top命令再按下大键盘的1,可以看到每个CPU的使用率是比较平均的

4 进程间通信方式

管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
以上几种进程间通信方式中,消息队列是使用的比较频繁的方式。

(1)管道pipe

import multiprocessing

def foo(conn):
  conn.send('hello father')  #向管道pipe发消息
  print(conn.recv())

if __name__ == '__main__':
  conn1,conn2=multiprocessing.Pipe(True)  #开辟两个口,都是能进能出,括号中如果False即单向通信
  p=multiprocessing.Process(target=foo,args=(conn1,)) #子进程使用sock口,调用foo函数
  p.start()
  print(conn2.recv()) #主进程使用conn口接收,从管道(Pipe)中读取消息
  conn2.send('hi son') #主进程使用conn口发送

(2)消息队列Queue

Queue是多进程的安全队列,可以使用Queue实现多进程之间的数据传递。

Queue的一些常用方法:

  • Queue.qsize():返回当前队列包含的消息数量;
  • Queue.empty():如果队列为空,返回True,反之False ;
  • Queue.full():如果队列满了,返回True,反之False;
  • Queue.get():获取队列中的一条消息,然后将其从列队中移除,可传参超时时长。
  • Queue.get_nowait():相当Queue.get(False),取不到值时触发异常:Empty;
  • Queue.put():将一个值添加进数列,可传参超时时长。
  • Queue.put_nowait():相当于Queue.get(False),当队列满了时报错:Full。

案例:

from multiprocessing import Process, Queue
import time


def write(q):
  for i in ['A', 'B', 'C', 'D', 'E']:
   print('Put %s to queue' % i)
   q.put(i)
   time.sleep(0.5)


def read(q):
  while True:
   v = q.get(True)
   print('get %s from queue' % v)


if __name__ == '__main__':
  q = Queue()
  pw = Process(target=write, args=(q,))
  pr = Process(target=read, args=(q,))
  print('write process = ', pw)
  print('read process = ', pr)
  pw.start()
  pr.start()
  pw.join()
  pr.join()
  pr.terminate()
  pw.terminate()

Queue和pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据。

注:进程间通信应该尽量避免使用共享数据的方式

5 多进程实现生产者消费者

以下通过多进程实现生产者,消费者模式

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


class MultiProcessProducer(multiprocessing.Process):
  def __init__(self, num, queue):
   """Constructor"""
   multiprocessing.Process.__init__(self)
   self.num = num
   self.queue = queue

  def run(self):
   t1 = time.time()
   print('producer start ' + str(self.num))
   for i in range(1000):
     self.queue.put((i, self.num))
   # print 'producer put', i, self.num
   t2 = time.time()

   print('producer exit ' + str(self.num))
   use_time = str(t2 - t1)
   print('producer ' + str(self.num) + ', 
   use_time: '+ use_time)



class MultiProcessConsumer(multiprocessing.Process):
  def __init__(self, num, queue):
   """Constructor"""
   multiprocessing.Process.__init__(self)
   self.num = num
   self.queue = queue

  def run(self):
   t1 = time.time()
   print('consumer start ' + str(self.num))
   while True:
     d = self.queue.get()
     if d != None:
      # print 'consumer get', d, self.num
      continue
     else:
      break
   t2 = time.time()
   print('consumer exit ' + str(self.num))
   print('consumer ' + str(self.num) + ', use time:' + str(t2 - t1))


def main():
  # create queue
  queue = multiprocessing.Queue()

  # create processes
  producer = []
  for i in range(5):
   producer.append(MultiProcessProducer(i, queue))

  consumer = []
  for i in range(5):
   consumer.append(MultiProcessConsumer(i, queue))

  # start processes
  for i in range(len(producer)):
   producer[i].start()

  for i in range(len(consumer)):
   consumer[i].start()

  # wait for processs to exit
  for i in range(len(producer)):
   producer[i].join()

  for i in range(len(consumer)):
   queue.put(None)

  for i in range(len(consumer)):
   consumer[i].join()

  print('all done finish')


if __name__ == "__main__":
  main()

6 总结

​ python中的多进程创建有以下两种方式:

(1)fork子进程

(2)采用 multiprocessing 这个库创建子进程

​ 需要注意的是队列中queue.Queue是线程安全的,但并不是进程安全,所以多进程一般使用线程、进程安全的multiprocessing.Queue()

​ 另外, 进程池使用 multiprocessing.Pool实现,pool = multiprocessing.Pool(processes = 3),产生一个进程池,pool.apply_async实现非租塞模式,pool.apply实现阻塞模式。

apply_async和 apply函数,前者是非阻塞的,后者是阻塞。可以看出运行时间相差的倍数正是进程池数量。

​ 同时可以通过result.append(pool.apply_async(func, (msg, )))获取非租塞式调用结果信息的。

以上就是Python 多进程原理及实现的详细内容,更多关于python 多进程的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python 中文乱码问题深入分析
Mar 13 Python
快速入门python学习笔记
Dec 06 Python
Python实现简易版的Web服务器(推荐)
Jan 29 Python
python实现朴素贝叶斯分类器
Mar 28 Python
Python应用库大全总结
May 30 Python
Python迭代器与生成器基本用法分析
Jul 26 Python
Django contenttypes 框架详解(小结)
Aug 13 Python
python消除序列的重复值并保持顺序不变的实例
Nov 08 Python
python 工具 字符串转numpy浮点数组的实现
Mar 14 Python
Python类型转换的魔术方法详解
Dec 23 Python
将不规则的Python多维数组拉平到一维的方法实现
Jan 11 Python
pytorch 如何使用amp进行混合精度训练
May 24 Python
python-图片流传输的思路及示例(url转换二维码)
Dec 21 #Python
python 用pandas实现数据透视表功能
Dec 21 #Python
python 生成正态分布数据,并绘图和解析
Dec 21 #Python
python statsmodel的使用
Dec 21 #Python
Python 实现集合Set的示例
Dec 21 #Python
Python 实现二叉查找树的示例代码
Dec 21 #Python
如何利用Python matplotlib绘制雷达图
Dec 21 #Python
You might like
利用PHP和AJAX创建RSS聚合器的代码
2007/03/13 PHP
用php+javascript实现二级级联菜单的制作
2008/05/06 PHP
php简单实现sql防注入的方法
2016/04/22 PHP
Windows2003下php5.4安装配置教程(Apache2.4)
2016/06/30 PHP
PHP面试常用算法(推荐)
2016/07/22 PHP
分享一道笔试题[有n个直线最多可以把一个平面分成多少个部分]
2012/10/12 Javascript
javascript 获取图片尺寸及放大图片
2013/09/04 Javascript
得到form下的所有的input的js代码
2013/11/07 Javascript
jquery插件qrcode在线生成二维码
2015/04/26 Javascript
javascript委托(Delegate)blur和focus用法实例分析
2015/05/26 Javascript
jQuery实现的调整表格行tr上下顺序
2016/01/10 Javascript
jQueryUI Datepicker组件设置日期高亮
2016/10/13 Javascript
简易的JS计算器实现代码
2016/10/18 Javascript
微信小程序 wxapp内容组件 icon详细介绍
2016/10/31 Javascript
详解开源的JavaScript插件化框架MinimaJS
2017/10/26 Javascript
基于JavaScript实现抽奖系统
2018/01/16 Javascript
vue动态改变背景图片demo分享
2018/09/13 Javascript
详解关于webpack多入口热加载很慢的原因
2019/04/24 Javascript
vscode 调试 node.js的方法步骤
2020/09/15 Javascript
Vue中用JSON实现刷新界面不影响倒计时
2020/10/26 Javascript
[10:07]2014DOTA2国际邀请赛 实拍选手现场观战DK对阵Titan
2014/07/12 DOTA
[15:39]教你分分钟做大人:龙骑士
2014/10/30 DOTA
Python中用Ctrl+C终止多线程程序的问题解决
2013/03/30 Python
Python selenium如何设置等待时间
2016/09/15 Python
Python urls.py的三种配置写法实例详解
2017/04/28 Python
深入浅析Python获取对象信息的函数type()、isinstance()、dir()
2018/09/17 Python
在win10和linux上分别安装Python虚拟环境的方法步骤
2019/05/09 Python
python生成requirements.txt的两种方法
2019/09/18 Python
执行Python程序时模块报错问题
2020/03/26 Python
Sneaker Studio乌克兰:购买运动鞋
2018/03/26 全球购物
德国咖啡批发商:Coffeefair
2019/08/26 全球购物
卫校中专生的自我评价
2014/01/15 职场文书
中队活动总结
2014/08/27 职场文书
委托书的写法
2014/09/16 职场文书
医院护士工作检讨书
2014/10/26 职场文书
《落花生》教学反思
2016/02/16 职场文书