Python之路(第三十七篇)并发编程:进程、multiprocess模块、创建进程方式、join()、守护进程

一、在python程序中的进程操作

 之前已经了解了很多进程相关的理论知识,了解进程是什么应该不再困难了,运行中的程序就是一个进程。所有的进程都是通过它的父进程来创建的。因此,运行起来的python程序也是一个进程,那么也可以在程序中再创建进程。多个进程可以实现并发效果,也就是说,当程序中存在多个进程的时候,在某些时候,就会让程序的执行速度变快。在python中实现多进程需要借助python中强大的模块。

二、multiprocess模块

python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在python中大部分情况需要使用多进程。Python提供了multiprocessing。 multiprocessing模块用来开启子进程,并在子进程中执行我们定制的任务(比如函数),该模块与多线程模块threading的编程接口类似。

  multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

​ 需要再次强调的一点是:与线程不同,进程没有任何共享状态,进程修改的数据,改动仅限于该进程内。

Process类的介绍

创建进程的类

  Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)
  ​
  强调:
  1. 需要使用关键字的方式来指定参数
  2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号

  

参数介绍:

  
  group参数未使用,值始终为None
  ​
  target表示调用对象,即子进程要执行的任务
  ​
  args表示调用对象的位置参数元组,args=(1,2,'egon',)
  ​
  kwargs表示调用对象的字典,kwargs={'name':'egon','age':18}
  ​
  name为子进程的名称

  

方法介绍:

  
  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开启的进程 
 

  

属性介绍:

  
  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字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功(了解即可)

  

Process类的使用

注意:在windows中Process()必须放到# if name == 'main':下

创建并开启子进程的两种方式

方式一

  
  #方式一:直接用函数
  ​
  import multiprocessing
  # from multiprocessing import Process  这种导入模块的方式可以在下面代码中直接写Process(target= ,args=)
  import time
  ​
  def hi(name):
      print("hello %s"%name)
      time.sleep(1)
  ​
  if __name__ == "__main__":
      p = multiprocessing.Process(target=hi,args=("nick",))
      p.start()
      p.join()
      print("ending...")

  


方式二

  
  #开启进程的方式二,在类中启动进程
  import time
  import multiprocessing
  ​
  class Foo(multiprocessing.Process):  #这里继承 multiprocessing.Process类
  ​
      def __init__(self,name):
          super().__init__()
          self.name = name
  ​
      def run(self):
          print("hello %s" % self.name)
          time.sleep(3)
  ​
  if __name__ == "__main__":
      p = Foo("nick")
      p.start()  #这里执行start()会直接调用类的run()方法
      # p.join()
      print("ending...")

  

进程直接的内存空间是隔离的

  
  import multiprocessing
  ​
  n = 100
  ​
  ​
  def work():
      global n
      n = 0
      print("子进程内的n", n)
  ​
  ​
  if __name__ == '__main__':
      p = multiprocessing.Process(target=work)
      p.start()
      print("主进程内的n", n)

  

输出结果

  
  主进程内的n 100
  子进程内的n 0

  

分析:由于子进程和主进程是隔离的,所以即使在子进程里有global关键字,主进程同一变量的值也没变。

Process对象的join方法

在主进程运行过程中如果想并发地执行其他的任务,我们可以开启子进程,此时主进程的任务与子进程的任务分两种情况

情况一:在主进程的任务与子进程的任务彼此独立的情况下,主进程的任务先执行完毕后,主进程还需要等待子进程执行完毕,然后统一回收资源。

情况二:如果主进程的任务在执行到某一个阶段时,需要等待子进程执行完毕后才能继续执行,就需要有一种机制能够让主进程检测子进程是否运行完毕,在子进程执行完毕后才继续执行,否则一直在原地阻塞,这就是join方法的作用。

例子

  
  from multiprocessing import Process
  import time
  ​
  def func(args):
      print('-----',args)
      time.sleep(2)
      print("end---")
  ​
  if __name__ == '__main__':
      p = Process(target=func,args=(1,))
      p.start()
      print('哈哈哈哈')
      p.join()#join()的作用是阻塞主进程,使得主进程等待子进程执行完才把自己结束
      print("主进程运行完了")

  

例子2

  
  from multiprocessing import Process
  import time
  import random
  def piao(name):
      print('%s is talking' %name)
      time.sleep(random.randint(1,3))
      print('%s is talking end' %name)
  ​
  p1=Process(target=piao,args=('nick',))
  p2=Process(target=piao,args=('jack',))
  p3=Process(target=piao,args=('pony',))
  p4=Process(target=piao,args=('charles',))
  ​
  p1.start()
  p2.start()
  p3.start()
  p4.start()
  ​
  #疑问:既然join是等待进程结束,那么像下面这样写,进程不就又变成串行的了吗?
  #当然不是了,必须明确:p.join()是让谁等?
  #很明显p.join()是让主线程等待p的结束,卡住的是主线程而绝非进程p,
  ​
  #详细解析如下:
  #进程只要start就会在开始运行了,所以p1-p4.start()时,系统中已经有四个并发的进程了
  #而p1.join()是在等p1结束,没错p1只要不结束主线程就会一直卡在原地,这也是问题的关键
  #join是让主线程等,而p1-p4仍然是并发执行的,p1.join的时候,其余p2,p3,p4仍然在运行,等#p1.join结束,可能p2,p3,p4早已经结束了,这样p2.join,p3.join.p4.join直接通过检测,无需等待
  # 所以4个join花费的总时间仍然是耗费时间最长的那个进程运行的时间
  p1.join()
  p2.join()
  p3.join()
  p4.join()
  ​
  print('主线程')
  ​
  ​
  #上述启动进程与join进程可以简写为
  # p_l=[p1,p2,p3,p4]
  #
  # for p in p_l:
  #     p.start()
  #
  # for p in p_l:
  #     p.join()
 

  

Process对象的其他方法或属性

进程对象的其他方法:terminate与is_alivename与pid

例子

  
  import multiprocessing
  import time
  ​
  ​
  def task(name, t):
      print("进程正在运行。。。%s--%s" % (name, time.asctime()))
      time.sleep(t)
      print("进程结束了%s--%s" % (name, time.asctime()))
  ​
  ​
  if __name__ == "__main__":
      p1 = multiprocessing.Process(target=task, args=("nick", 5), name="进程1")  # 注意这里的参数name不是函数的参数
      p2 = multiprocessing.Process(target=task, args=("nicholas", 2), name="进程2")
      p_list = []
      p_list.append(p1)
      p_list.append(p2)
      print("进程是否存活", p1.is_alive())
      for p in p_list:
          p.start()
          print(p.name)  # 进程.name输出进程的名称
          print(p.name, p.pid)  # 进程对象.pid也可以输出子进程的ID号,
      print("进程是否存活", p1.is_alive())  # 判断子进程是否存活
      # p2.terminate()#不管进程是否执行完,立刻结束进程
      for p in p_list:
          p.join()
      print("ending...")

  

通过os模块查看pid和ppid

import multiprocessing
import os

class Foo(multiprocessing.Process):

    def __init__(self,name):
        super().__init__()
        self.name = name

    def run(self):
        print("hi %s,子进程号是%s"%(self.name,os.getpid()))#输出当前的进程的pid


if __name__ == "__main__":
    p = Foo("nick")
    p.start()
    print("主进程是%s"%os.getppid())  #这里的主进程好就是执行这个py文件的程序,这里是pycharm,
    # 如果用命令终端执行py文件则主进程是命令终端的号
    #os.getppid()是输出当前进程的父进程pid号
    #os.getpid()是输出当前的进程的pid

  

僵尸进程与孤儿进程(了解)

  一:僵尸进程(有害)
    僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。详解如下
  ​
  我们知道在unix/linux中,正常情况下子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束,如果子进程一结束就立刻回收其全部资源,那么在父进程内将无法获取子进程的状态信息。
  ​
  因此,UNⅨ提供了一种机制可以保证父进程可以在任意时刻获取子进程结束时的状态信息:
  1、在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)
  2、直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
  ​
    任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理。这是每个子进程在结束时都要经过的阶段。如果子进程在exit()之后,父进程没有来得及处理,这时用ps命令就能看到子进程的状态是“Z”。如果父进程能及时 处理,可能用ps命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。  如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
  ​
  二:孤儿进程(无害)
  ​
    孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
  ​
    孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
    
    三:僵尸进程危害场景:
  ​
    例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。
    
    
    

  

守护进程

会随着主进程的结束而结束。

主进程创建守护进程

  其一:守护进程会在主进程代码执行结束后就终止

  其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children

注意:进程之间是互相独立的,主进程代码运行结束,守护进程随即终止,不管守护进程执行没执行完。

例子1

  
  from multiprocessing import Process
  ​
  import time
  def foo():
      print(123)
      time.sleep(1)
      print("end123")
  ​
  def bar():
      print(456)
      time.sleep(3)
      print("end456")
  ​
  if __name__ == '__main__':
      p1=Process(target=foo)
      p2=Process(target=bar)
  ​
      p1.daemon=True ##一定要在p1.start()前设置,设置p1为守护进程,
      # 禁止p1创建子进程,并且父进程代码执行结束,p1即终止运行
      p1.start()
      p2.start()
      print("main-------") #只要终端打印出这一行内容,那么守护进程p也就跟着结束掉了

  

例子2

  
  # 主进程代码运行完毕,守护进程就会结束
  from multiprocessing import Process
  ​
  import time
  ​
  ​
  def foo():
      print(123)
      time.sleep(1)
      print("end123")
  ​
  ​
  def bar():
      print(456)
      time.sleep(3)
      print("end456")
  ​
  ​
  if __name__ == '__main__':
  ​
      p1 = Process(target=foo)
      p2 = Process(target=bar)
  ​
      p1.daemon = True
      p1.start()
      p2.start()
      print("main-------")  # 打印该行则主进程代码结束,则守护进程p1应该被终止,
      # 可能会有p1任务执行的打印信息123,因为主进程打印main----时,p1也执行了,但是随即被终止
      #这时p1可能会被执行,也可能不会被执行

  

参考资料

[1]http://www.cnblogs.com/Anker/p/3271773.html

猜你喜欢

转载自www.cnblogs.com/Nicholas0707/p/10183120.html