python8-多进程

1,概念

  • 操作系统分类

    • 多道批处理系统
      • 遇到I/O就切换
      • 可以提高CPU的利用率
      • 进程之间数据隔离(内存隔离)
      • 时空复用:在同一个时间点上,多个程序同时执行(时间复用,有的在做CPU操作,有的在做I/O操作);一块内存中存储了多个进程的数据(空间复用,但进程之间的数据是隔离的)
    • 分时操作系统
      • 时间分片
      • 时间片的轮转
    • 实时操作系统
  • 进程

    • 基本概念

      • 运行中的程序就是一个进程
      • 需要占用资源,受操作系统调度
      • 每个进程都有对应的pid,是进程的唯一标识,在其生命周期内不变
      • 进程是计算机中最小的资源分配单位(分配一些内存)
      • 进程之间数据隔离
      • 可以利用多核
    • 进程的三状态

      • 进程三状态:就绪/Ready、运行/Running、阻塞/Blocked

        • 就绪:进程已经分配到CPU以外所有必要的资源,只要活得处理机便可立即执行,这时的进程状态称为就绪状态。
        • 运行:进程已经获得处理机,其程序正在处理机上执行,此时的进程状态称为运行状态。
        • 阻塞:正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机处于阻塞状态。引起阻塞的事件有:等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。

    • 进程的调度算法

      • 给所有进程分配资源及CPU使用权的方法,例如:短作业优先、先来先服务、多级反馈算法(多个任务队列,优先级由高到低,先来的任务总是优先级最高的,每一个新任务几乎会立即获得一个时间片时间,执行完一个时间片之后都会降低到下一级队列中,总是优先级高的任务都执行完才执行优先级低的队列,优先级越高时间片越短)
    • 进程的开启和结束

      • 开启/创建进程的形式:
        • 系统初始化
        • 进程在运行过程中创建子进程
        • 用户交互式请求创建新进程
        • 批处理作业的初始化
        • 无论哪种创建形式,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的(父进程开启子进程,父进程负责回收子进程的资源)
      • 结束形式:
        • 正常退出(linux中用exit, windows中用ExitProcess)
        • 出错退出(自愿,如python a,py 而a. py不存在、assert None)
        • 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以通过 try... except... 捕获)
        • 被其他进程杀死(如 kill pid)
  • 线程

    • 线程是进程中的一个单位,不能脱离进程存在
    • 线程是bz计算机中能被CPU调度的最小单位(cpu执行的是编译解释之后线程中的代码)
  • 并发

    • 多个程序同时执行但只有一个cpu,多个程序轮流在一个cpu上执行
    • 宏观上是多个程序同时执行
    • 微观上是多个程序轮流在一个cpu上执行,本质上还是串行
  • 并行

    • 多个程序同时执行并且有多个cpu,多个程序同时在多个cpu上执行
    • 宏观和微观上都是多个程序在各自的cpu上同时执行
  • 同步(必须等结果)

    • 在执行任务A的时候发起任务B,必须等到任务B结束之后才能继续执行任务A
  • 异步(不必等结果)

    • 在执行任务A的时候发起任务B,但不需要等待任务B结束就可以继续执行任务A
  • 阻塞

    • CPU不工作
  • 非阻塞

    • CPU在工作
  • 同步阻塞、异步非阻塞、异步阻塞、异步非阻塞(同步阻塞与否是针对本进程,异步阻塞与否是针对其他进程)

    • 同步阻塞:调用函数需要等待函数的执行结果,并且在执行函数的的过程中CPU不工作(input,sleep,recv,recvfrom, connect, get)
    • 同步非阻塞:调用函数需要等待函数的执行结果,并且在执行函数的的过程中CPU工作(eval('1+2+3+4'), sum, max, min, sorted)
    • 异步阻塞:调用函数不需要立即获取函数的执行结果,可以去做其他的事情,但是如果没有结果返回,有一个或多个进程是被阻塞的(生产者消费者模型:多个生产者是异步的,但是如果队列中没有数据,在消费者就会堵塞)
      • 一个例子:开启3个异步进程获取数据,哪一个进程先结束就先获取哪个进程的返回值并开启后续的处理进程;但是如果没有结果返回,后续的处理进程就要等着直到前面的进程有结果返回。
    • 异步非阻塞:调用函数不需要立即获取函数的执行结果,并且执行结果获得与否并不影响其他进程的执行(start)

2,常用命令

2.1,Process

if '__name__' == '__main__':
    while True:
    	multiprocessing.Process(target=func, args=(arg,)).start()
'''例子1'''
from multiprocessing import Process
import os

def func():
    print(os.getpid(), os.getppid())

if __name__ == '__main__':	#windows操作系统不加报错,mac上不加也可以
    print('main', os.getpid(), os.getppid())
    p = Process(target=func)
    p.start()

  • 为什么windows不加 if _name_ == '__main__' 会报错?

    • windows操作系统下:

      • 在windows操作系统下,开启一个子进程会import父进程所在的文件
      • 在本例的子进程中,os、Process、func都会加载进来,而由于if _name_ == '__main__' 只有在本文件下运行时才满足,因此其下代码都不运行,但是子进程会执行func函数
      • 如果子进程中if _name_ == '__main__' 下的代码依然能运行,那会不断生成并运行子进程,进入一个死循环
      • windows操作系统下,只会在主进程执行的所有代码写在if _name_ == '__main__' 之下,不然会出各种问题
    • linux操作系统下:

      • 在linux操作系统下,开启一个子进程会将父进程内存空间所有的内容copy到自己的内存空间中
    • 两者区别:

      from multiprocessing import Process
      
      def func():
          print(os.getpid(), os.getppid())
      
      if __name__ == '__main__':
          import os
          print('main', os.getpid(), os.getppid())
          p = Process(target=func)
          p.start()
      

      上面这段代码在windows下会报错,linux下不报错。

      from multiprocessing import Process
      import os
      
      def func():
          print(os.getpid(), os.getppid())
      print(123)
      
      if __name__ == '__main__':
          print('main', os.getpid(), os.getppid())
          p = Process(target=func)
          p.start()
      

      上面这段代码在windows下会打印两次123,linux下只打印一次123

  • 如何给子进程传递参数?

    p = Process(target=func, args=())			args参数传递必须跟元组
    
    from multiprocessing import Process
    import os
    
    def func(name, age):
        print(os.getpid(), os.getppid(), name, age)
    
    if __name__ == '__main__':
        print('main', os.getpid(), os.getppid())
        p = Process(target=func, args=('zhangkaiyu', 24))
        p.start()
    
  • 能不能获取子进程的返回值?不能

  • 如何开启多个子进程? 多次创建子进程,然后start就行了(异步非阻塞)

    from multiprocessing import Process
    import os
    import time
    import random
    
    def func(name, age):
        print(f'发邮件给{age}岁的{name}')
        deltaTime = random.randint(1, 5)
        time.sleep(deltaTime)
        print(f'{name}的邮件发送完毕,耗时{deltaTime}s')
    
    if __name__ == '__main__':
        argList = [('zhangkaiyu', 24), ('zky', 24), ('kaiyu', 24)]
        for arg in argList:
            p = Process(target=func, args=arg)
            p.start()   #开启子进程,并不等待子进程执行完,直接向下继续执行(典型的异步非阻塞)
    
  • join的用法,阻塞直到调用join方法的子进程执行完毕再继续执行代码(同步阻塞)

    from multiprocessing import Process
    import os
    import time
    import random
    
    def func(name, age):
        print(f'发邮件给{age}岁的{name}')
        deltaTime = random.randint(1, 5)
        time.sleep(deltaTime)
        print(f'{name}的邮件发送完毕,耗时{deltaTime}s')
    
    if __name__ == '__main__':
        argList = [('zhangkaiyu', 24), ('zky', 24), ('kaiyu', 24)]
        pList = []
        for arg in argList:
            p = Process(target=func, args=arg)
            pList.append(p)
            p.start()
        for p in pList:
            p.join()	#所有的子进程都要join
        print('所有邮件发送完毕!')
    
  • 同步阻塞就是join,异步非阻塞就是start

  • 多进程之间的数据是隔离的

    from multiprocessing import Process
    n = 0
    def func():
        global n
        n += 1
        print(n)
    
    if __name__ == '__main__':
        pList = []
        for i in range(100):
            p = Process(target = func)
            pList.append(p)
            p.start()
        for p in pList:
            p.join()
        print(n)
    

    子进程的打印结果是1,主进程的打印结果是0,因为数据是隔离的。

  • 使用多线程实现一个并发的socket的server

    '''例子一,server可以为多个client提供服务'''
    '''server'''
    import socket
    from multiprocessing import Process
    
    def talk(conn):
        while True:
            msg = conn.recv(1024).decode('utf-8')
            ret = msg.upper().encode('utf-8')
            conn.send(ret)
        conn.close()
    
    if __name__ == '__main__':
        sk = socket.socket()
        sk.bind(('10.181.22.132', 9001))
        sk.listen()
        while True:
            '''最关键的点'''
            conn, addr = sk.accept()		
            Process(target=talk, args=(conn,)).start()
        sk.close()
        
    
    '''client'''
    import time
    import socket
    
    sk = socket.socket()
    sk.connect(('10.181.22.132', 9001))
    
    while True:
        sk.send(b'hello')
        msg = sk.recv(1024).decode('utf-8')
        print(msg)
        time.sleep(0.5)
    
    sk.close()
    
  • 开启进程的另一种方法——面向对象

    import os
    from multiprocessing import Process
    import time
    
    class MyProcess(Process):
        def __init__(self, args):
            super().__init__()
            self.args = args
        def run(self):
            print(os.getpid(), os.getppid(), self.args)
            time.sleep(1)
            print('end')
    
    if __name__ == '__main__':
        print(os.getpid(), os.getppid())
        for i in range(10):
            p = MyProcess((1,))
            p.start()
    
  • Process类的一些其他方法和属性

    p = Process(target=func, args=(,)) / 或者通过面向对象获取的子进程
    p.pid()									        获取子进程的pid
    p.name()									获取子进程的名字
    p.ident()									获取子进程的pid(与p.pid相同)
    p.terminate()								        强制结束一个子进程
    p.is_alive()								        查看一个子进程是否还活着
    
    import os
    from multiprocessing import Process
    import time
    
    class MyProcess(Process):
        def __init__(self, args):
            super().__init__()
            self.args = args
        def run(self):
            print(os.getpid(), os.getppid(), self.args)
            time.sleep(1)
            print('end')
    
    if __name__ == '__main__':
        print(os.getpid(), os.getppid())
        for i in range(10):
            p = MyProcess((1,))
            p.start()
            print(p.pid)
            print(p.name)
            print(p.ident)
            p.terminate()
            time.sleep(0.1)
            print(p.is_alive())
    

3,守护进程

在start一个进程之前设置
p.daemon = True

主进程会等待所有的子进程结束,是为了回收子进程的资源。

而守护进程会等待主进程的代码执行结束之后再结束,为不是等待整个主进程结束。(守护进程依旧是子进程,不可能在主进程结束后再结束,因为需要主进程回收资源)

主进程的代码结束守护进程结束,和其他子进程的执行无关。

一般情况下,多个进程的执行顺序可能是:

主进程代码结束——》守护进程结束——》子进程结束——》主进程结束(三个高亮的相对顺序必须是这样)

子进程结束——》主进程代码结束——》守护进程结束——》主进程结束

'''例子一'''
import time
from multiprocessing import Process

def son1():
    while True:
        print('in son1')
        time.sleep(1)

if __name__ == '__main__':
    p = Process(target=son1)
    p.daemon = True
    p.start()
    time.sleep(5)
    print('in main')
'''例子二'''
import time
from multiprocessing import Process

def son1():
    while True:
        print('in son1')
        time.sleep(1)

def son2():
    for i in range(10):
        print('-->in son2')
        time.sleep(1)

if __name__ == '__main__':
    p1 = Process(target=son1)
    p1.daemon = True
    p1.start()
    Process(target=son2).start()
    time.sleep(5)
    print('in main')
    '''
    在本例中,in son1打印5次,-->in son2打印10次,守护进程并没有等整个主进程结束后再结束,而是主进程所有代码执行完后就结束了
    '''
'''例子三'''
import time
from multiprocessing import Process

def son1():
    while True:
        print('in son1')
        time.sleep(1)

def son2():
    for i in range(10):
        print('-->in son2')
        time.sleep(1)

if __name__ == '__main__':
    p1 = Process(target=son1)
    p1.daemon = True
    p1.start()
    p2 = Process(target=son2)
    p2.start()
    time.sleep(5)
    print('in main')
    p2.join()
   
'''等待p2结束——》主进程的代码才结束——》守护进程p1结束'''

4,进程同步/进程之间数据安全——互斥锁 Lock

  • 保证数据安全,会降低程序运行效率
  • 单个锁不能在一个进程中acquire多次
from multiprocessing import Lock

lock = Lock()						#锁只能有一把
lock.acquire()						#拿钥匙(没有拿到钥匙的子进程只能被阻塞,等待拿走钥匙的进程release)
'''锁起来的代码,如果代码出错,不异常处理,钥匙就没了'''
lock.release()						#还钥匙

'''高级写法'''
with lock:
    '''锁起来的代码,而且这种写法被锁起来的代码出错,钥匙自动release'''
'''一个数据不安全的例子'''
import json
from multiprocessing import Process
import time

def buy(i):
    with open('ticket', encoding='utf-8') as f:
        ticket = json.load(f)
    if ticket['count'] > 0:
        ticket['count'] -= 1
        print('{}买到票了,还剩{}张票'.format(i, ticket['count']))
    time.sleep(0.1)
    with open('ticket', mode='w', encoding='utf-8') as f:
        json.dump(ticket, f)

if __name__ == '__main__':
    for i in range(10):
        p = Process(target=buy, args=(i,))
        p.start()
'''买票数据安全'''
import json
from multiprocessing import Process, Lock
import time

def search(i):
    with open('ticket', encoding='utf-8') as f:
        ticket = json.load(f)
    print('{}:当前的余票是{}张'.format(i, ticket['count']))

def buy(i):
    with open('ticket', encoding='utf-8') as f:
        ticket = json.load(f)
    if ticket['count'] > 0:
        ticket['count'] -= 1
        print('\033[32m-->{}买到票了,还剩{}张票\033[0m'.format(i, ticket['count']))
        time.sleep(0.1)
        with open('ticket', mode='w', encoding='utf-8') as f:
            json.dump(ticket, f)
    else:
        print('\033[31m-->很遗憾,{}没有买到票\033[0m'.format(i))

def ticketAPP(i, lock):
    search(i)
    lock.acquire()
    buy(i)
    lock.release()

if __name__ == '__main__':
    lock = Lock()
    for i in range(15):
        p = Process(target=ticketAPP, args=(i, lock))
        p.start()

5,进程之间通信(IPC: Inter Process Communication)——队列

  • 基于文件:同一台机器上的多个进程之间通信(基于socket的文件级别的通信完成数据传递,Queue:基于socket、pickle、Lock实现,天然的数据安全;此外multiprocessing中还有Pipe:基于socket、pickle实现,数据不安全)

  • 基于网络:同一台机器或者多台机器上的多进程间通信(消息中间件:memcache,redis,rabbitmq,kafka)

    from multiprocessing import Queue
    
    '''例子1'''
    from multiprocessing import Queue, Process
    
    def son(q):
        q.put('hello')
    
    if __name__ == '__main__':
        q = Queue()
        p = Process(target=son, args=(q,))
        p.start()
        print(q.get())  #子进程put的数据在主进程可以get,主进程和子进程的Queue共享
    
    '''例子2'''
    from multiprocessing import Queue, Process
    
    def pro(q):
        print('-->', q.get())
        print(123)
        print('-->', q.get())   #队列为空,get阻塞
    
    def son(q):
        q.put('hello')
    
    if __name__ == '__main__':
        q = Queue()
        p1 = Process(target=son, args=(q,))
        p1.start()
        p2 = Process(target=pro, args=(q,))
        p2.start()
    
    '''例子3'''
    from multiprocessing import Queue, Process
    
    def pro(q):
        for i in range(5):
            print(q.get())
    
    def son(q):
        for i in range(10):
            q.put(f'hello{i}')
    
    if __name__ == '__main__':
        q = Queue()
        p1 = Process(target=son, args=(q,))
        p1.start()
        p2 = Process(target=pro, args=(q,))
        p2.start()
    
  • 生产者消费者模型:将原本获取数据处理数据的完整过程进行了解耦,让生成数据和消费数据的效率达到平衡并且最大化(提高执行效率,降低并发数,降低系统压力)

    '''例子1'''
    from multiprocessing import Queue, Process
    import time
    import random
    
    def consumer(q):
        '''消费者:通常取得数据之后还要进行某些操作'''
        for i in range(10):
            print(q.get())
    
    def producer(q):
        '''生产者:通常放数据之前需要通过某些操作获取数据'''
        for i in range(10):
            q.put(i)
            time.sleep(random.randint(1,5))
    
    if __name__ == '__main__':
        q = Queue()
        p1 = Process(target=producer, args=(q,))
        p2 = Process(target=consumer, args=(q,))
        p1.start()
        p2.start()
    
    '''例子2'''
    from multiprocessing import Process, Queue
    import time
    import random
    
    def producer(q, name, food):
        for i in range(10):
            time.sleep(random.randint(1, 5))
            foodi = f'{food}{i}'
            print(f'\033[32m{name}生产了{foodi}\033[0m')
            q.put(foodi)
    
    def consumer(q, name):
        for i in range(10):
            time.sleep(random.randint(2, 4))
            print(f'\033[34m{name}吃了{q.get()}\033[0m')
    
    if __name__ == '__main__':
        q = Queue()
        p1 = Process(target=producer, args=(q, '罗翔', '蛋糕'))
        p2 = Process(target=consumer, args=(q, '张三'))
        p1.start()
        p2.start()
    
    '''例子3'''
    from multiprocessing import Process, Queue
    import time
    import random
    
    def producer(q, name, food):
        for i in range(4):
            time.sleep(random.randint(0, 2))
            foodi = f'{food}{i}'
            print(f'\033[32m{name}生产了{foodi}\033[0m')
            q.put(foodi)
    
    def consumer(q, name):
        while True:
            time.sleep(random.random())
            food = q.get()
            if food:
                if name == '张三':
                    print(f'\033[34m{name}吃了{food}\033[0m')
                else:
                    print(f'\033[35m{name}吃了{food}\033[0m')
            else:
                print(f'\033[36m{name}吃完了\033[0m')
                break
    
    if __name__ == '__main__':
        q = Queue()
        p = Process(target=producer, args=(q, '罗翔', '蛋糕'))
        c1 = Process(target=consumer, args=(q, '张三'))
        c2 = Process(target=consumer, args=(q, '法外狂徒'))
        p.start()
        c1.start()
        c2.start()
        p.join()
        q.put(None)
        q.put(None) #有几个消费者就要发几个None
    
    '''例子4:异步阻塞和生产者消费者模型'''
    import requests
    from multiprocessing import Process, Queue
    
    urlList = [
        'https://www.cnblogs.com/tensorzhang/p/14966383.html',
        'https://github.com/akashgit/VEEGAN/blob/master/VEEGAN_2D_RING.ipynb',
        'https://www.sciencedirect.com/science/article/pii/S0262885620301372',
        'https://xueshu.lanfanshu.cn/scholar?cluster=10661655492543733137&hl=zh-CN&as_sdt=0,5',
        'https://aistudio.baidu.com/aistudio/education/group/info/16651',
        'https://www.cnblogs.com/tensorzhang/p/14972650.html',
    ]
    
    def producer(i, url, q):
        ret = requests.get(url)
        q.put((i, ret.status_code))
    
    if __name__ == '__main__':
        q = Queue()
        for index, url in enumerate(urlList):
            Process(target=producer, args=(index, url, q)).start()
        for i in range(len(urlList)):   
            #异步阻塞,并没有按照顺序等待结果(所有任务都在异步执行着,要等结果,但是不知道谁的结果先回来,谁先回来先获取谁的结果)
            print(q.get())
    

6,进程之间数据共享——Manager

  • 进程之间的数据是隔离的

    '''由于数据隔离,子进程只是在开始时复制了主进程中的数据,之后互不影响'''
    from multiprocessing import Process
    
    def change_dic(dic):
        dic['count'] -= 1
        print(dic)
    
    if __name__ == '__main__':
        dic = {'count' : 100}
        p_l = []
        for i in range(100):
            p = Process(target=change_dic, args=(dic,))
            p.start()
            p_l.append(p)
        for p in p_l:
            p.join()
        print(dic)
    
  • manager提供了字典、列表等数据类型,可以做到进程之间数据共享,但是数据不安全

    from multiprocessing import Process, Manager
    
    def change_dic(dic):
        dic['count'] -= 1
        print(dic)
    
    if __name__ == '__main__':
        m = Manager()
        dic = m.dict({'count' : 100})
        p_l = []
        for i in range(100):
            p = Process(target=change_dic, args=(dic,))
            p.start()
            p_l.append(p)
        for p in p_l:
            p.join()
        print(dic)
        # with Manager() as m:
        #     lock = Lock()
        #     dic = m.dict({'count' : 100})
        #     p_l = []
        #     for i in range(100):
        #         p = Process(target=change_dic, args=(dic, lock))
        #         p.start()
        #         p_l.append(p)
        #     for p in p_l:
        #         p.join()
        #     print(dic)
    
  • 使用锁保证数据安全(但是这不就近似于同步了???),使用异步的一个重要原因就是数据隔离,如果存在大量的数据共享其实就不适合用异步了

    from multiprocessing import Process, Manager, Lock
    
    def change_dic(dic, lock):
        with lock:
            dic['count'] -= 1
        print(dic)
    
    if __name__ == '__main__':
        m = Manager()
        lock = Lock()
        dic = m.dict({'count' : 100})
        p_l = []
        for i in range(100):
            p = Process(target=change_dic, args=(dic, lock))
            p.start()
            p_l.append(p)
        for p in p_l:
            p.join()
        print(dic)
    

总结:

  • 如何开启进程?(两种方法:start-join、面向对象)

  • 进程之间存在数据不安全的问题,如何解决?(加锁:lock)

  • 进程之间如何通信ipc?(队列Queue-数据安全,管道Pipe-数据不安全,第三方工具)

  • 进程之间如何实现数据共享?(Manager类-数据不安全)

  • 如何使用守护进程?守护进程与主进程的关系?(daemon=True, 主进程代码结束——》守护进程结束——》主进程结束

  • 生产者消费者模型

posted @ 2021-07-03 15:00  tensor_zhang  阅读(107)  评论(0编辑  收藏  举报