11-03 多进程操作篇

一. multiprocessing模块介绍

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':下

详细解释

Since Windows has no fork, the multiprocessing module starts a new Python process and imports the calling module. 
If Process() gets called upon import, then this sets off an infinite succession of new processes (or until your machine runs out of resources). 
This is the reason for hiding calls to Process() inside

if __name__ == "__main__"
since statements inside this if-statement will not get called upon import.
由于Windows没有fork,多处理模块启动一个新的Python进程并导入调用模块。 
如果在导入时调用Process(),那么这将启动无限继承的新进程(或直到机器耗尽资源)。 
这是隐藏对Process()内部调用的原,使用if __name__ == “__main __”,这个if语句中的语句将不会在导入时

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

方法一:

from multiprocessing import Process
import time

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


# 提示: 两个进程的内存空间互相隔离
"""
创建进程之前注意事项:
    - Windows操作系统下创建进程一定要在__name__内创建, 因为在windows下创造进程类似于模块导入的方式,会从上往下依次执行代码.
    - linux操作系统中,会将代码重新拷贝一份,互不干扰.
"""
if __name__ == '__main__':
    # 创建进程的代码
    """
    p = Process(target=task, args=('jason', ))
        - 第1个参数是需要创建进程的目标
        - 第2个参数是为创建进程的目标进行传参
    """
    p = Process(target=task, args=('jason', ))

    p.start()  # 告诉操作系统帮你创建一个进程. ==> 这里是异步提交的方式
    print("主")

方法二: 继承Process类

from multiprocessing import Process
import time


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

    def run(self):  # 这里必须指定run名字
        print(f'{self.name} is running...')
        time.sleep(2)
        print(f'{self.name} is over...')


if __name__ == '__main__':
    p = MyProcess('jason')
    p.start()
    print('主')

# 总结:
"""
1. 创建进程就是在内存中申请一块内存空间,将需要运行的代码丢进去
2. 一个进程对应在内存中,就是一块独立的内存空间
3. 多个进程对应在内存中,就是多块独立的内存空间
4. 进程与进程之间'默认'情况下是无法直接交互的. 如果想交互可以借助第三方工具(模块).
"""

2. 验证: 进程间的内存空间是隔离的

from multiprocessing import Process

money = 100


def task():
    global money
    money = 666
    print('子进程:', money)  # 子进程: 666


if __name__ == '__main__':
    p = Process(target=task)
    p.start() 
    print('主进程:', money)  # 主进程: 100

3. join方法

# 作用: join让主进程的代码等待子进程代码运行结束之后,再继续运行. 不影响其它子进程的执行. 
from multiprocessing import Process
import time

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


if __name__ == '__main__':
    p = Process(target=task, args=('jason', ))
    p.start()  # 告诉操作系统帮你创建一个进程. ==> 这里是异步提交的方式
    # time.sleep(4)
    p.join()  # 主进程等待子进程p运行结束之后再继续往后执行.
    print("主")
    
"""
jason is running...
jason is over...
主
"""    

4. 有了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=('egon', 2))
    p3 = Process(target=task, args=('tank', 3))

    # 强调1: 这里的start仅仅是告诉操作系统要创建进程. 操作系统去帮你创建进程是随机的,并不会按以下顺序依次帮你去创建进程.
    start_time = time.time()
    p1.start()
    p2.start()
    p3.start()

    # 强调2(并行): 在join之前,start就把上面的三个进程已经向操作系统发起了请求,操作系统会确保这三个进程起来. 这三个进程是并行运行,
    p1.join()
    p2.join()
    p3.join()
    stop_time = time.time()
    print("示例一运行时间(并行):", stop_time-start_time)

    # 强调3(串行):  在join之前,start就把上面的三个进程已经向操作系统发起了请求,操作系统会确保这三个进程起来. 这三个进程是并行运行,
    p4 = Process(target=task, args=('jason1', 1))
    p5 = Process(target=task, args=('tank1', 2))
    p6 = Process(target=task, args=('alex1', 3))
    start_time = time.time()
    p4.start()
    p4.join()  # 从这个位置以后的代码都是属于主进程的, 因此下面代码不会执行, 会等待p4进程运行完毕, 才能运行下面的代码

    p5.start()
    p5.join()

    p6.start()
    p6.join()
    stop_time = time.time()
    print("示例二运行时间(串行):", stop_time-start_time)

    print("主")

"""
jason is running...
egon is running...
tank is running...
jason is over...
egon is over...
tank is over...
示例一运行时间(并行): 3.100740909576416
jason1 is running...
jason1 is over...
tank1 is running...
tank1 is over...
alex1 is running...
alex1 is over...
示例二运行时间(串行): 6.225710868835449
主
"""

5. Process对象的其他方法或属性(了解)

# 知识储备
"""
什么是PID号?
    每当计算机启动一个进程, 操作系统都会为该进程分配一个独一无二的PID号. 操作系统可以通过PID号对该进程进行标识. 进程结束时进程就会被操作系统回收. 
    
如何查看进程的PID号?
    1) Windows平台上在cmd命令行输入以下命令:
        - 查看所有的进程(包含PID信息): tasklist
        - 指定查看某一个进程的(包含PID信息): tasklist | findstr 进程名        
        
    2) linux平台在终端命令行中输入以下命令:
        - 查看所有的进程(包含PID信息): ps aus
        - 指定查看某一个进程的(包含PID信息): ps aus | grep -i 进程名
"""

# 方法介绍
"""
current_process().pid: 查看当前进程的进程PID号
os.getpid(): 查看当前进程的进程PID号(推荐)

os.getppid(): 查看当前进程的父进程PID号

p.terminate(): 杀死当前进程. 注意!!: 这句话是告诉操作系统去帮你去杀死进程,而操作系统帮你杀死进程,需要一定的时间间隔. 如果如果代码的时间间隔极快,那么执行这条命令以后并不会立即产生效果,如果需要查看需要指定一段时间间隔, 在一定时间间隔之后就可以查看的到.
p.is_alive(): 判断当前进程是否存活
"""

# 代码示例
from multiprocessing import Process
from multiprocessing import current_process  
import os
import time
   
def task():
    print(f'[task]查看当前进程的进程PID号: {current_process().pid}')
    print(f'[task]查看当前进程的进程PID号: {os.getpid()}')
    print(f'[task]查看当前进程的父进程PID号: {os.getppid()}')

if __name__ == '__main__':
    p = Process(target=task)
    p.start()
    p.join()

    print('[主]查看当前进程的进程PID号:', current_process().pid)
    print('[主]查看当前进程的进程PID号:', os.getpid())
    print('[主]查看当前进程的父进程PID号:', os.getppid())
    
    # 执行结果:
    '''
    [task]查看当前进程的进程PID号: 8124
    [task]查看当前进程的进程PID号: 8124
    [task]查看当前进程的父进程PID号: 1240
    [主]查看当前进程的进程PID号: 1240
    [主]查看当前进程的进程PID号: 1240
    [主]查看当前进程的父进程PID号: 6360
    '''

    p.terminate()
    time.sleep(2)
    print(p.is_alive())  # False

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

僵尸进程(有害): 主进程永远不结束, 子进程一直在开启, 子进程结束后不释放PID号, 子进程执行完毕 但是子进程的PID号一直占用, 该进程就变成了僵尸进程.
    1) 不立刻释放占用进程号原因: 因为要让父进程能够查看到他开设的子进程的一些基本信息,占用的pId号,运行时间等待其他基本信息.因此,状态下所有的进程都会步入僵尸进程,未让父进程可以查看子进程的,基本信息,
    2) 坏处: 父进程不死,并且在无限制的创建子进程,子进程进程号无法被父进程回收, 造成大量的操作系统PID号被无意义的占用, 很可能造成正常进程想要启动时无法被正常分配PID号, 进而无法启动.
    3) 如何回收子进程占用的pId号呢?
        - 父进程等待子进程运行结束, 父进程才结束, 进而让父进程收回子进程.
        - 父进程调用join方法: 等待进程执行完毕,join函数内部会发送系统调用wait,去告诉操作系统回收掉该进程
    
    
孤儿进程(无害): 子进程存活, 父进程意外死亡
    问题: 父进程意外死亡,子进程pId没有人回收,在子进程执行完毕以后, 没有父进程回收该子进程.
    解决: 操作系统'开设了一个儿童福利院',专门管理孤儿院进程,回收相关资源.

五. 守护进程

# 注意注意注意!!!: 代码执行完毕并不代表进程结束.

# 强调!!!: 父进程代码执行完毕, 守护进程会立即被回收, 但是父进程不会立即结束, 父进程会等待所有的非守护的子进程结束, 父进程会回收结束的子进程的PID号等资源, 回收完毕以后父进程才会结束. 
'''
什么是守护进程?
    父进程代码执行完毕, 守护进程立刻结束, 且同时被回收PID等资源. 举个例子: 古代皇帝死后,皇帝的妃子以及侍卫等等都需要陪葬.皇帝 -> 父进程, 陪葬的东西 -> 守护进程   

如何创建守护进程?
    由父进程创建守护进程
    注意!!!: 守护进程内无法再开启子进程, 否则抛出异常: # AssertionError: daemonic processes are not allowed to have children
                
    开启守护进程步骤注意!!!:    
        p.daemon = True  # 将进程p设置成守护进程. 注意: 这一句话必须要放在start的方法上面才有效.
        p.start()        
'''
from multiprocessing import Process
import time

def task(name):
    print(f'{name}总管正在活着')
    time.sleep(2)
    print(f'{name}总管正在死亡')

if __name__ == '__main__':
    p = Process(target=task, args=('jason', ))
    p.daemon = True
    p.start()

    print('皇帝jason寿终正寝.')

    # 执行结果:
    '''
    皇帝jason寿终正寝.
    '''

迷惑人的例子:

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("主")  # 主进程代码结束, 守护进程p1就立即结束. 同时因为操作系统对进程的开启需要一定的时间, 因此p1中的代码并不会被执行.

    # 执行结果:
    '''
    主
    456
    end456
    '''

六. 互斥锁

'''
问题: 针对多个人操作同一份数据的时候会出现数据错乱的问题.
解决: 针对这种数据错乱的问题,解决方式就是加锁处理,将并发变成串行,虽然牺牲了效率,但是保证了数据的安全性.

拓展: 行锁, 表锁
    行锁: 有人操作这一行数据的时候,对这一行数据加锁处理.
    表锁: 有人操作这个表的时候,对着个表进程加锁处理.
    
注意!!!: 
1. 锁不要轻易的使用,容易造成死锁现象.
2. 锁只在处理数据的部分加, 用来来保证数据的安全.(只在增强数据的环节加锁处理即可)    
'''

# 创建data.json文件, 写入以下内容:
"""
{"ticket_num": 3}
"""

import json
import time
import random
from multiprocessing import Process
from multiprocessing import Lock


def search_ticket(user):  # ticket  /ˈtɪkɪt/  票 车票 票券
    with open('data.json', 'rt', encoding='utf8') as f:
        dic = json.load(f)
    print(f'用户{user}查询余票. 余票剩余:{dic.get("ticket_num")}张')


def buy_search(user):
    # 先查票
    with open('data.json', 'rt', encoding='utf8') as f:
        dic = json.load(f)

    # 模拟网络延迟
    time.sleep(random.randint(1, 3))

    # 判断当前是否有票
    if dic.get('ticket_num') > 0:
        # 修改数据库买票
        dic['ticket_num'] -= 1
        # 写入数据库
        with open('data.json', 'wt', encoding='utf8') as f:
            json.dump(dic, f)
        print(f"{user}购票成功!")
    else:
        print(f'{user}购票失败!')

def run(user, mutex):
    search_ticket(user)

    # 给买票环节加锁处理,
    # 1. 抢锁
    mutex.acquire()
    buy_search(user)
    # 2. 释放锁
    mutex.release()


if __name__ == '__main__':
    # 在主进程中生成一把锁,让所有的子进程抢,谁抢到谁就先执行买票功能,
    mutex = Lock()   # mutex n. 互斥;互斥元,互斥体;互斥量
    for i in range(1, 10):
        p = Process(target=run, args=(f'用户{i}', mutex))
        p.start()

七. 进程间通信机制

进程间通信机制简称IPC机制. IPC 英文全称 Inter-Process Communication

1. 队列

# 储备知识:
"""
队列模块: Quque
队列: 先进先出 FIFO
堆栈: 先进后出 FILO


导入的两种形式:
from multiprocessing import Queue
import queue

拓展: 本地测试的时候才可能会用到Queue,实际生产用的都是别人封装好的功能非常强大的工具: redis, kafuka, RQ
"""

# 代码示例:
import queue
# 1. 创建一个队列
q = queue.Queue(5)  # 括号内可以传数字,表示生成的队列大可以同时存放的数据量. 不传默认值2147483647

# 2. 往队列中存数据
# 注意!!!: 存取数据存是为了更好的取. 千方百计的存,就是为了能简单快捷的取
q.put(111)
q.put(222)
q.put(333)
q.put(444)
q.put(555)
# q.put(666)  # 当队列数据放满了之后 如果还有数据要放程序会阻塞 直到有位置让出来 不会报错.
# print(q.full())  # 判断当前队列是否满了
# print(q.empty()))  # 判断当前队列是否空了

# 3. 往队列中取数据
print(q.get())
print(q.get())
print(q.get())
print(q.get())
print(q.get())
# print(q.get())  # 队列中如果已经没有数据的话 get方法会原地阻塞
# print(q.get_nowait())   # 没有数据直接报错 queue.Empty
# print(q.get(timeout=3))  # 没有数据之后原地等待三秒之后再报错 queue.Empty

# 用异常捕获处理队列中的数据被取空的情况
try:
    print(q.get(timeout=3))
except queue.Empty:
    print('管道q中的数据已经被取空了!')
    
# 总结:
"""
# 方法总结
q.put(数据): 往队列中放置数据. 如果放置数据次数大于队列长度, 将会堵塞在原地.

q.get(): 从队列中取出数据. 如果队列中数据被取空了, 再取将会堵塞在原地.
q.get(timeout=取数据等待时间): 从队列中取出数据. 如果队列中数据被取空了, 等待一段时间, 没有数据以后抛出异常: queue.Empty
q.get_nowait(): 从队列中取出数据. 如果队列中数据被取空了, 直接抛出异常: queue.Empty

q.full(): 判断队列是否满了
q.empty(): 判断队列是为空


# 下面三种方法在多进程的情况下是不精确的. 因为多个进程都共用一个管道, 无法时时针对管道的数据情况的判断.
    q.full()
    q.empty()
    q.get_nowait()
"""

2. 进程间通信

'''
研究思路
    1. 主进程跟子进程借助于队列通信,
    2. 子进程跟子进程借助于队列通信
疑问: 多进程之间去基于同一个队列取值会不会取乱?
    因为基于IPC通信机制Queue锁+管道组成.只有被第一个取值完毕第二个才能取值(锁: 串行).     
'''

# 主进程与子进程之间借助队列通信
from multiprocessing import Process
from multiprocessing import Queue

def producer(q):  # producer /prəˈdjuːsə(r)/ 制作人,制片人;生产者;发生器
    q.put('我是23号技师,很高兴为您服务!')
    print('hello big baby~')

if __name__ == '__main__':
    q = Queue()
    p = Process(target=producer, args=(q, ))
    p.start()

    print(q.get())
    '''
    hello big baby~
    我是23号技师,很高兴为您服务!
    '''
    
    
# 子进程与子进程之间借助队列通信
from multiprocessing import Process
from multiprocessing import Queue

def producer(q):  # producer /prəˈdjuːsə(r)/ 制作人,制片人;生产者;发生器
    q.put('我是23号技师,很高兴为您服务!')
    print('hello big baby~')
    
def consumer(q): #  consumer /kənˈsjuːmə(r)/ 消费者;用户,顾客
    print(q.get())

if __name__ == '__main__':
    q = Queue()
    p = Process(target=producer, args=(q, ))
    p1 = Process(target=consumer, args=(q, ))
    p.start()
    p1.start()
    # 执行结果:
    '''
    hello big baby~
    我是23号技师,很高兴为您服务!
    ''' 

九. 生产者消费者模型

'''
什么是生产者?什么是消费者?
    生产者指的是: 负责生产或者说制造数据
    消费者指的是: 负责消费或者说处理数据

为什么要有生产者,消费者模型?
    用专业的话说:
        在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
    
    用简洁的话说: 
        生产者消费者模型就是基于提供的中间媒介(队列), 达到双方之间处理数据互不干扰, 进而独立高效率的完整自己的事情.
     
    举例: 生产者生产包子,消费者购买包子. 不基于其他附属情况下, 如果包子铺生产的包子,一直在做包子阶段,而包子铺需要做很多包子,那么购买包子的消费者,就必须等待,只有等所有包子生产完毕以后,消费者才能进行购买包子,因此我们需
    要一种中间媒介来解决这种问题. 解决方法就是生产者把包子放在蒸笼里,消费者从蒸笼里购买包子,这就形成了生产者生产的包子能立即被消费者购买, 两者之间互不影响, 进而提升了效率.
    
表现形式:
    生产者 ----> 队列 -----> 消费者
    
    
JoinableQueue使用思路:
提示: 这里的q队列对象是可以被等待的, 类似于进程方法"p.join"的功能.
1. 往队列中放数据q.put(数据)的时候, 内部会有一个计数器自动加1
2. 从队列中取数据q.get()的时候, 取数据完毕调用q.task_done()让内部的计数器自动减1
3. q.join()只有当队列为空时或者说计数器为0时, 才会继续往下运行代码
'''

from multiprocessing import Process
# from multiprocessing import Queue
from multiprocessing import JoinableQueue
import time
import random

def producer(name, food, q):
    for i in range(5):
        data = f'生产者{name}生产了{food}{i}'
        time.sleep(random.randint(1, 3))  # 模拟生产包子的中间时间
        print(data)
        q.put(f'{food}{i}')

def consumer(name, q):
    while True:
        food = q.get()
        time.sleep(random.randint(1, 3))  # 模拟消费者陆续过来购买包子的时间间隔
        print(f'消费者{name}购买了{food}')
        q.task_done()   # 告诉队列你已经从里面取出了一个数据并且处理完毕了. 计数器将队列中的数据-1.

if __name__ == '__main__':
    # q = Queue()
    q = JoinableQueue()

    p1 = Process(target=producer, args=('egon', '包子', q))
    p2 = Process(target=producer, args=('tank', '包子', q))

    c1 = Process(target=consumer, args=('egon', q))
    c2 = Process(target=consumer, args=('egon', q))
    
    p1.start()
    p2.start()
    c1.daemon = True  # 将消费者设置成守护进程, 当所有生产者执行完毕所有的数据被消费者从队列中取数据完毕, 进而控制主进程的结束, 来控制没有存在必要性的所有消费者守护进程结束
    c2.daemon = True
    c1.start()
    c2.start()

    p1.join()  # 这里2个join做的事情是: 上面的所有生产者子进程和所有的消费者子进程都向操作系统发送了开启请求, 这里要等所有的生产者生产完毕以后才执行以下代码, 保证队列中因为生产者来不及执行, 导致队列为空或者数据没生产完毕, 进而导致下面的q.join方法判断队列中数据为空向下执行以后结束了主进程, 进而也把所有的消费者守护进程结束.
    p2.join()

    # 解决方式一: 在生产者执行完毕以后, 往队列中放对应的结束信号'None', 让每个消费者获取到None以后结束.(缺点: 有几个消费者就必须往队列中放几个结束信号.)
    # q1.put(None)  # 肯定在所有生产者生产完毕正常的数据的末尾, 因为p1.join的控制, 执行到了这一行就代表生产者生产完毕了.
    # q2.put(None) 
    
    # 解决方式二: 利用JoinableQueue队列模式的计数器判断队列的数据是否被取干净 + 对消费者进行守护线程设置达到所有生产者生产完毕,队列且被取干净从而结束主进程, 进而主进程的所有消费者守护进程结束.
    q.join()  # 等待队列中所有的数据被取完再执行往下执行代码. 这里的作用是: 让主进程等待队列中所有的数据被消费者取完毕以后, 也就是说所有的生产者不再生产新数据了, 所有的消费者把所有的生产者生产的数据都取完毕以后. 才执行下一行代码. 下一行没有代码了, 主进程结束了, 进而所有消费者守护进程也要结束, 因为队列中的数据都被取完了, 消费者没有存在的必要了.
posted @ 2020-04-22 22:58  给你加马桶唱疏通  阅读(189)  评论(0编辑  收藏  举报