进程的创建、join方法、进程中的方法、僵尸进程与孤儿进程、互斥锁

今日学习内容总结

      在昨日的学习中,我们已经通过操作系统的发展史来理解学习并行,并发的相关知识。包括进程的理论,同步与异步,阻塞与非阻塞的学习。而今天的主要学习内容就是对通过代码实现进程,并对进程中的各种属性,方法的一个理解。

代码创建进程

      在昨日的学习中我们已经理解了进程其实就是计算机中的一段代码正在执行的过程。而我们要学习的就是通过代码创建进程的方式。

      process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。而process模块的语法是:

  Process([group [, target [, name [, args [, kwargs]]]]])

      语法中的参数介绍:

  group:参数未使用,默认值为None。

  target:表示调用对象,即子进程要执行的任务。

  args:表示调用的位置参数元组。注意:args指定的为传给target函数的位置参数,是一个元祖形式,必须有逗号。

  kwargs:表示调用对象的字典。如kwargs = {'name':Jack, 'age':18}。

  name:子进程名称。

      Process属性方法介绍

方法/属性 描述
start() 启动进程,调用进程中的run()方法。
run() 进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 。
terminate() 强制终止进程,不会进行任何清理操作。如果该进程终止前,创建了子进程,那么该子进程在其强制结束后变为僵尸进程;如果该进程还保存了一个锁那么也将不会被释放,进而导致死锁。使用时,要注意。
is_alive() 判断某进程是否存活,存活返回True,否则False。
join([timeout]) 主线程等待子线程终止。timeout为可选择超时时间;需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 。
daemon 默认值为False,如果设置为True,代表该进程为后台守护进程;当该进程的父进程终止时,该进程也随之终止;并且设置为True后,该进程不能创建子进程,设置该属性必须在start()之前
name 进程名称。
pid 进程pid
exitcode 进程运行时为None,如果为-N,表示被信号N结束了。
authkey 进程身份验证,默认是由os.urandom()随机生成32字符的字符串。这个键的用途是设计涉及网络连接的底层进程间的通信提供安全性,这类连接只有在具有相同身份验证才能成功。

      创建进程的两种方式:

  # 第一种创建方式
  import os
  from multiprocessing import Process
   
  def func_one():
      print("This is son_one")
      print("son_one:%s  father:%s" % (os.getpid(), os.getppid()))
   
  if __name__ == '__main__':
      p_one = Process(target=func_one)
      p_one.start()
      print("son:%s  father:%s" % (os.getpid(), os.getppid()))

  # 打印结果
  ''''
  son:9756  father:10912
  This is son_one
  son_one:3328  father:9756
  '''

  # 第二种创建方式  以继承Process的方式开启进程的方式
  import os
  from multiprocessing import Process

  class MyProcess(Process):
      def __init__(self, name):
          super().__init__()
          self.name = name

      def run(self):
          print("进程为%s,父进程为%s" % (os.getpid(), os.getppid()))
          print("我的名字是%s" % self.name)

  if __name__ == '__main__':
      p_one = MyProcess('张三')
      p_two = MyProcess('李四')
      p_thr = MyProcess('王五')

      p_one.start()  # 自动调用run()
      p_two.start()
      p_thr.run()  # 直接调用run()

      p_one.join()
      p_two.join()
      # p_thr.join()  # 调用run()函数的不可以调用join()

      print("主进程结束")

  # 打印结果
  '''
  进程为7708,父进程为10912
  我的名字是王五
  进程为10388,父进程为7708
  我的名字是李四
  进程为444,父进程为7708
  我的名字是张三
  主进程结束
  '''

      process类的使用说明:

      在windows中Process()必须放到# if name == 'main':下。由于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。这是隐藏对Process()内部调用的原,使用if name == “__main __”,这个if语句中的语句将不会在导入时被调用。

join方法

      join方法,就是让主进程代码等待子进程代码运行完毕后再执行的方法。

join方法的简单使用

from multiprocessing import Process
import time

def task(name, n):
    print(f'{name} is running')
    time.sleep(n)
    print(f'{name} is over')

if __name__ == '__main__':
    p1 = Process(target=task, args=('jason', 1))
    p2 = Process(target=task, args=('tony', 2))
    p3 = Process(target=task, args=('kevin', 3))
    start_time = time.time()
    p1.start()
    p2.start()
    p3.start()
    p1.join()
    p2.join()
    p3.join()
    end_time = time.time() - start_time
    print('主进程', f'总耗时:{end_time}')

  # 执行结果
  '''
  jason is running
  tony is running
  kevin is running
  jason is over  # 1s后出现
  tony is over  # 又过1s
  kevin is over  # 又过1s
  主进程 总耗时:3.093252182006836
  '''  

      通过上述代码,能够比较直观的看出join方法的效果。如果是一个start一个join交替执行 那么总耗时就是各个任务耗时总和。

      当时有个疑问,既然join是等待进程结束,那么我像下面这样写,进程不就又变成串行的了吗?事实上当然不是,必须明确:p.join()是让谁等?很明显p.join()是让主线程等待p的结束,卡住的是主线程而绝非进程p。

      代码解析:进程只要start就会在开始运行了,所以p1-p3.start()时,系统中已经有三个个并发的进程了。而我们p1.join()是在等p1结束,没错p1只要不结束主线程就会一直卡在原地,这也是问题的关键。join是让主线程等,而p1-p3仍然是并发执行的,p1.join的时候,其余p2,p3仍然在运行,等p1.join结束,可能p2,p3早已经结束了,这样p2.join,p3.join直接通过检测,无需等待。所以3个join花费的总时间仍然是耗费时间最长的那个进程运行的时间。结果为3s。

进程间的数据默认隔离

  from multiprocessing import Process

  money = 999

  def task():
      global money  # 局部修改全局不可变类型
      money = 666
      print('子进程内:', money)


  if __name__ == '__main__':
      p = Process(target=task)
      p.start()  # 子进程内: 666
      p.join()  # 确保子进程代码运行结束再打印money
      print('主进程内:', money)  # 主进程内: 999

进程对象中查看进程号的方法

    # 方式1  current_process函数
    from multiprocessing import Process, current_process

    print(current_process().pid)  # 通过这种方式就可以查看进程号了

    # 获取进程号的用处之一就是可以通过代码的方式管理进程
    # 通过进程号终止进程
    Windows:taskkill关键字  
    mac/Linux: kill关键字

    # 在os模块中获取进程号
    os.getpid()  # 获取当前进程的进程号
    os.getppid()  # 获取当前进程的父进程号

僵尸进程与孤儿进程

僵尸进程

      一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。所有的子进程在运行结束之后都会变成僵尸进程,还保留着pid和一些运行过程的中的记录便于主进程查看(短时间保存)。

孤儿进程

      一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。子进程会被操作系统自动接管。

守护进程

      守护进程就是会随着主进程的结束而结束的进程,具有以下两个特点:

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

      实例:

  from multiprocessing import Process
  import time
   
   
  def func_one():
      print("func_one")
      time.sleep(2)
      print("End func_one")
   
   
  def func_two():
      print("func_two")
      time.sleep(3)
      print("End func_two")
   
   
  if __name__ == '__main__':
      p_one = Process(target=func_one)
      p_two = Process(target=func_two)
   
      p_one.daemon = True
      p_one.start()
      p_two.start()
      time.sleep(0.1)  # 时间太短,导致print("End func_one")无法打印
   
      print("主进程结束")

  # 打印结果
  '''
  func_one
  func_two
  主进程结束
  End func_two
  '''
  # 进程之间是互相独立的,主进程代码运行结束,守护进程随即终止

互斥锁

      进程之间数据不共享,但是共享同一套文件系统,所以访问同一个文件,或同一个打印终端,是没有问题的,而共享带来的是竞争,竞争带来的结果就是错乱,如何控制,就是加锁处理。

  # 并发运行,效率高,但竞争同一打印终端,带来了打印错乱
  from multiprocessing import Process
  import os,time
  def work():
      print('%s is running' %os.getpid())
      time.sleep(2)
      print('%s is done' %os.getpid())

  if __name__ == '__main__':
      for i in range(3):
          p=Process(target=work)
          p.start()
  # 打印结果
  '''
  5448 is running
  6112 is running
  11224 is running
  5448 is done
  11224 is done
  6112 is done

  Process finished with exit code 0

'''

# 由并发变成了串行,牺牲了运行效率,但避免了竞争
from multiprocessing import Process,Lock
import os,time
def work(lock):
    lock.acquire()
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())
    lock.release()
if __name__ == '__main__':
    lock=Lock()
    for i in range(3):
        p=Process(target=work,args=(lock,))
        p.start()
  # 打印结果
  '''
  11144 is running
  11144 is done
  9788 is running
  9788 is done
  3480 is running
  3480 is done
  '''

      这里用一个模拟抢票来体现互斥锁的作用。

  # 代码模拟抢票(有问题)
  import json
  from multiprocessing import Process
  import time
  import random


  # 查票
  def search(name):
      with open(r'ticket_data.json', 'r', encoding='utf8') as f:
          data = json.load(f)
      print(f'{name}查询当前余票:%s' % data.get('ticket_num'))


  # 买票
  def buy(name):
      '''
      点击买票是需要再次查票的 因为期间其他人可能已经把票买走了
      '''
      # 1.查票
      with open(r'ticket_data.json', 'r', encoding='utf8') as f:
          data = json.load(f)
      time.sleep(random.randint(1, 3))
      # 2.判断是否还有余票
      if data.get('ticket_num') > 0:
          data['ticket_num'] -= 1
          with open(r'ticket_data.json', 'w', encoding='utf8') as f:
              json.dump(data, f)
          print(f'{name}抢票成功')
      else:
          print(f'{name}抢票失败 没有余票了')


  def run(name):
      search(name)
      buy(name)


  # 模拟多人同时抢票
  if __name__ == '__main__':
      for i in range(1, 10):
          p = Process(target=run, args=('用户:%s' % i,))
          p.start()

  # 这时候运行你会发现,每个人查询余票都是1.但是每个人都抢票成功了。这就是当多个进程操作同一份数据的时候会造成数据的错乱。所以需要加锁处理。

  # 加锁处理
  from multiprocessing import Process, Lock
  
  def run(name, lock):
      lock.acquire()  # 抢锁
      search(name)
      buy(name)
      lock.release()  # 放锁

  if __name__ == '__main__':
      lock = Lock()
      for i in range(1, 10):
          p = Process(target=run, args=('用户:%s' % i, lock))
          p.start()
  # 打印结果
  '''
  用户:1查询当前余票:1
  用户:1抢票成功
  用户:2查询当前余票:0
  用户:2抢票失败 没有余票了
  用户:4查询当前余票:0
  用户:4抢票失败 没有余票了
  用户:3查询当前余票:0
  用户:3抢票失败 没有余票了
  用户:5查询当前余票:0
  用户:5抢票失败 没有余票了
  用户:8查询当前余票:0
  用户:8抢票失败 没有余票了
  用户:9查询当前余票:0
  用户:9抢票失败 没有余票了
  用户:6查询当前余票:0
  用户:6抢票失败 没有余票了
  用户:7查询当前余票:0
  用户:7抢票失败 没有余票了
  '''

      加锁处理将并发变成串行,牺牲了效率但是保证的数据的安全。互斥锁并不能轻易使用,容易造成死锁现象。互斥锁只在处理数据的部分加锁,不能什么地方都加,严重影响程序的效率。

posted @ 2022-04-19 20:32  くうはくの白  阅读(65)  评论(0)    收藏  举报