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版学生管理系统
Jan 10 Python
Python随机函数random()使用方法小结
Apr 29 Python
有关Python的22个编程技巧
Aug 29 Python
python提取包含关键字的整行数据方法
Dec 11 Python
解决python3 安装不了PIL的问题
Aug 16 Python
python3 常见解密加密算法实例分析【base64、MD5等】
Dec 19 Python
简单了解django处理跨域请求最佳解决方案
Mar 25 Python
matplotlib.pyplot.matshow 矩阵可视化实例
Jun 16 Python
基于Tensorflow的MNIST手写数字识别分类
Jun 17 Python
零基础小白多久能学会python
Jun 22 Python
Python 中 sorted 如何自定义比较逻辑
Feb 02 Python
python b站视频下载的五种版本
May 27 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 配置文件中open_basedir选项作用
2009/07/19 PHP
解析php5配置使用pdo
2013/07/03 PHP
PHP 正则表达式常用函数
2014/08/17 PHP
Laravel 5框架学习之路由、控制器和视图简介
2015/04/07 PHP
在次封装easyui-Dialog插件实现代码
2010/11/14 Javascript
jquery插件制作 手风琴Panel效果实现
2012/08/17 Javascript
javascript删除字符串最后一个字符
2014/01/14 Javascript
理运用命名空间让js不产生冲突避免全局变量的泛滥
2014/06/15 Javascript
JQuery实现表格动态增加行并对新行添加事件
2014/07/30 Javascript
JS中三目运算符和if else的区别分析与示例
2014/11/21 Javascript
JQuery中属性过滤选择器用法实例分析
2015/05/18 Javascript
javascript实现dom元素可拖动
2016/03/21 Javascript
jquery的ajax提交form表单的两种方法小结(推荐)
2016/05/25 Javascript
vue元素实现动画过渡效果
2017/07/01 Javascript
webstorm中配置Eslint的两种方式及差异比较详解
2018/10/19 Javascript
vue-router实现嵌套路由的讲解
2019/01/19 Javascript
在vue中使用jsx语法的使用方法
2019/09/30 Javascript
微信小程序使用GoEasy实现websocket实时通讯
2020/05/19 Javascript
vantUI 获得piker选中值的自定义ID操作
2020/11/04 Javascript
python的else子句使用指南
2016/02/27 Python
Python中的浮点数原理与运算分析
2017/10/12 Python
Python中enumerate函数代码解析
2017/10/31 Python
使用python实现BLAST
2018/02/12 Python
NumPy.npy与pandas DataFrame的实例讲解
2018/07/09 Python
Python如何应用cx_Oracle获取oracle中的clob字段问题
2019/08/27 Python
python 使用elasticsearch 实现翻页的三种方式
2020/07/31 Python
Python读取Excel一列并计算所有对象出现次数的方法
2020/09/04 Python
用python发送微信消息
2020/12/21 Python
美国球鞋寄卖网站:Stadium Goods
2018/05/09 全球购物
介绍一下HTTP、HTTPS和SSL
2012/12/16 面试题
linux面试题参考答案(1)
2016/01/22 面试题
酒店led欢迎词
2014/01/09 职场文书
如何做好工作总结!
2019/04/10 职场文书
pytest进阶教程之fixture函数详解
2021/03/29 Python
jdbc使用PreparedStatement批量插入数据的方法
2021/04/27 MySQL
python操作xlsx格式文件并读取
2021/06/02 Python