【4.4】进程间通信(队列和生产消费模型)

【一】引入

【1】什么是进程间通信(Inter-Process Communication, IPC)

  • 进程间通信(Inter-Process Communication, IPC)是指两个或多个进程之间进行信息交换的过程。
  • 它是一种计算机编程技术,用于在不同进程之间共享数据和资源。

【2】如何实现进程间通信

  • 借助于消息队列,进程可以将消息放入队列中,然后由另一个进程从队列中取出。
  • 这种通信方式是非阻塞的,即发送进程不需要等待接收进程的响应即可继续执行。
  • multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的

【3】什么是管道

  • 管道是一种半双工的通信机制,即只能在一个方向上进行数据传输。
  • 子进程可以通过继承父进程的管道来实现通信。
  • stdin、stdout和stderr是Python中的三个内置文件对象,它们分别代表标准输入、标准输出和标准错误。
  • 这些对象也可以作为管道使用。
  • 当我们在一个进程中使用read方法读取管道内的消息后,其他进程将无法再获取该管道内的任何其他消息。
  • 因此,我们需要使用锁或其他同步机制来确保多个进程能够正确地访问和修改共享资源。

【4】什么是队列(管道 + 锁)

  • 队列是一种线程安全的数据结构,它支持在多线程环境中高效地实现生产者-消费者模型。
  • 队列的特性是先进先出(First-In-First-Out, FIFO),即先插入队列的数据将先被取出。
  • 堆栈是一种后进先出(Last-In-First-Out, LIFO)的数据结构,与队列相反,最后插入的数据将首先被取出。

【5】进程间通信的目的

  • 存是为了更好的取
  • 千方百计的存
  • 简单快捷的取

【二】队列介绍(推荐使用)

【1】创建队列的类(底层就是以管道和锁定的方式实现)

(1)语法

import queue

q = queue.Queue(maxsize)
  • Queue([maxsize]):
    • 创建共享的进程队列
    • Queue是多进程安全的队列
    • 可以使用Queue实现多进程之间的数据传递。

(2)参数介绍

  • maxsize是队列中允许最大项数,省略则无大小限制。

【2】方法介绍

(1)主要方法

  • q.put

    • 用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。
    • 如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常
    • 如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
  • q.get

    • 可以从队列读取并且删除一个元素,同样,get方法有两个可选参数:blocked和timeout。
    • 如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。
    • 如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
  • q.get_nowait()

    • 同q.get(False)
  • q.put_nowait()

    • 同q.put(False)
  • q.empty()

    • 调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
  • q.full()

    • 调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
  • q.qsize()

    • 返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样

(2)其他方法(了解)

  • q.cancel_join_thread()

    • 不会在进程退出时自动连接后台线程。可以防止join_thread()方法阻塞
  • q.close()

    • 关闭队列,防止队列中加入更多数据。
    • 调用此方法,后台线程将继续写入那些已经入队列但尚未写入的数据,但将在此方法完成时马上关闭。
    • 如果q被垃圾收集,将调用此方法。
    • 关闭队列不会在队列使用者中产生任何类型的数据结束信号或异常。
    • 例如,如果某个使用者正在被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。
  • q.join_thread()

    • 连接队列的后台线程。
    • 此方法用于在调用q.close()方法之后,等待所有队列项被消耗。
    • 默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread方法可以禁止这种行为

【3】方法示例示例

(1)q.get():队列无数据不报错

# 【一】导入队列模块
import queue

# 【二】创建队列
# 括号内可以传参数,表示生成的队列最大可以同时存放的数据量
# 默认 为 0
q = queue.Queue(5)

# 【三】向队列中放数据
q.put(111)

# 【四】在队列中取数据
res_get = q.get()

# 【五】获取到结果
print(res_get)
# 111
  • 当队列数据放满了以后,如果还有数据要放程序就会阻塞,直到有位置让出来(不会报错)
  • 队列中如果已经没有数据的话,程序也会阻塞

(2)q.get_nowait():队列无数据报错

# 【一】导入队列模块
import queue

# 【二】创建队列
# 括号内可以传参数,表示生成的队列最大可以同时存放的数据量
# 默认 为 0
q = queue.Queue(5)

# 【三】向队列中放数据
q.put(111)
q.put(222)

# 【四】在队列中取数据
res_get_one = q.get()
res_get_two = q.get()
res_get_three = q.get_nowait()

# 【五】获取到结果
print(res_get_one)
print(res_get_two)
print(res_get_three)

# 当队列中的数据已经被取空后,使用 get_nowait() 会触发报错
# raise Empty : _queue.Empty

(3)q.get(timeout=3):延迟抛出异常

# 【一】导入队列模块
import queue

# 【二】创建队列
# 括号内可以传参数,表示生成的队列最大可以同时存放的数据量
# 默认 为 0
q = queue.Queue(5)

# 【三】向队列中放数据
q.put(111)
q.put(222)

# 【四】在队列中取数据
res_get_one = q.get()
res_get_two = q.get()
# 设置延时时间,推迟 指定 s 数报错
res_get_three = q.get(timeout=3)

# 【五】获取到结果
print(res_get_one)
print(res_get_two)
print(res_get_three)

# 当队列中的数据已经被取空后,使用 get_nowait() 会触发报错
# raise Empty : _queue.Empty

(4)q.full():判断当前队列数据是否填满队列

# 【一】导入队列模块
import queue

# 【二】创建队列
# 括号内可以传参数,表示生成的队列最大可以同时存放的数据量
# 默认 为 0
q = queue.Queue(2)

# 【三】向队列中放数据
print(q.full())  # False

q.put(111)
print(q.full())  # False

q.put(222)
print(q.full())  # True

# 【四】在队列中取数据
res_get_one = q.get()
res_get_two = q.get()

# 【五】获取到结果
print(res_get_one)
print(res_get_two)

(5)q.empty():判断当前队列是否为空

# 【一】导入队列模块
import queue

# 【二】创建队列
# 括号内可以传参数,表示生成的队列最大可以同时存放的数据量
# 默认 为 0
q = queue.Queue(2)

# 【三】向队列中放数据
print(q.full())  # False

q.put(111)
print(q.full())  # False

q.put(222)
print(q.full())  # True

# 【四】在队列中取数据
print(q.empty())  # False

res_get_one = q.get()
print(q.empty())  # False

res_get_two = q.get()
print(q.empty())  # True

# 【五】获取到结果
print(res_get_one)
print(res_get_two)

【4】小结

# 导入模块
import queue

# 创建队列桶
q = queue.Queue(指定桶的容量)

# 向队列中添加数据
q.put(放入的数据类型)

# 判断当前队列是否满了,满了返回  True 否则为 False
q.full()

# 从队列中取出数据,队列中无数据不会报错,但是会夯住
data = q.get()

# 从队列中取出数据,队列中无数据会在指定延迟时间后抛出异常:raise Empty : _queue.Empty
data = q.get(timeout=秒数)

# 从队列中取出数据,队列中无数据会抛出异常:raise Empty : _queue.Empty
data = q.get_nowait()

# 判断当前队列是够空了,空了返回  True 否则为 False
q.empty()
  • 以上方法在多进程 的情况下不精确
  • 多进程的队列切换时间是非常短的,存在可能判断失误

【三】进程间通信(IPC机制)

【1】什么是IPC机制

  • IPC机制指进程间通信机制(Inter-Process Communication),它是指在不同进程间传输数据或者信息的一种机制。
  • 在多进程操作系统中,各个进程相互独立,不能直接访问对方的内存空间,所以必须通过特殊的通信方式实现进程之间的信息交换和协调。
  • 常见的IPC机制包括管道、消息队列、共享内存和信号量等方式。
  • 其中,管道、消息队列和共享内存都是用于进程之间的数据传输
  • 而信号量则是用于进程之间的同步与互斥。
  • 通过使用IPC机制,可以使得不同进程能够有效地协同工作,在复杂的程序设计中起到十分重要的作用。

【2】子进程与主进程之间通过队列进行通信

(1)需求

  • 主进程与子进程之间借助于队列进行通信

(2)实现

from multiprocessing import Process, Queue

# 创建一个子进程函数
def producer(que_ue):
    # 子进程放入消息
    que_ue.put('子进程中放入了一条消息')
    print('这是主进程中的一个子进程的队列')


if __name__ == '__main__':
    # 【一】创建队列对象:注意这里是使用的 multiprocessing 里面的 Queue
    que_ue = Queue()
    # 【二】创建子进程:目标函数和参数带进去
    pro_cess = Process(target=producer, args=(que_ue,))
    # 【三】启动子进程
    pro_cess.start()
    # 【四】主进程获取消息
    msg_from_p = que_ue.get()

    # 【五】查看消息
    print(msg_from_p)
    # 这是主进程中的一个子进程的队列
    # 子进程中放入了一条消息

【2】子进程与子进程之间借助队列进行通信

(1)需求

  • 子进程与子进程之间借助于队列进行通信.

(2)实现

from multiprocessing import Process, Queue


# 创建一个子进程函数
def producer(que_ue):
    # 子进程放入消息
    que_ue.put('子进程中放入了一条消息')
    print('这是主进程中的一个子进程的队列')


def customer(que_ue):
    # 在另一个子进程中获取到非本子进程放入的消息
    msg_from_producer = que_ue.get()
    print('这是来自另一个子进程的消息 :>>>> ', msg_from_producer)


if __name__ == '__main__':
    # 【一】创建队列对象:注意这里是使用的 multiprocessing 里面的 Queue
    que_ue = Queue()
    # 【二】创建子进程:目标函数和参数带进去
    p_list = []
    pro_producer = Process(target=producer, args=(que_ue,))
    pro_customer = Process(target=customer, args=(que_ue,))
    # 【三】启动子进程
    pro_producer.start()
    pro_customer.start()

    pro_producer.join()
    pro_customer.join()

    # 这是主进程中的一个子进程的队列
    # 这是来自另一个子进程的消息 :>>>>  子进程中放入了一条消息

【四】生产者和消费者模型

【1】理论

(1)生产者模型

  • 生产者模型和消费者模型是指通过利用队列解耦生产者和消费者的一种并发编程模型。
  • 在生产者模型中,生产者负责将数据放入共享队列中,而消费者则从队列中取出数据进行处理。
  • 生产者和消费者之间通过共享这个队列来进行信息的交流。
  • 这种模型适用于生产者和消费者之间的处理速度不一致的情况,同时还能够保证数据传输的安全性和正确性。

(2)消费者模型

  • 在消费者模型中,消费者负责向队列中插入任务,而由线程池中的工作线程进行任务的处理。
  • 消费者和工作线程之间通过共享线程池中的任务队列来完成任务分发和执行。
  • 这种模型适用于任务处理需要一定时间的情况,能够充分利用多线程的优势提高系统的并发性能,提高效率。

(3)小结

  • 生产者:生产/制造东西
  • 消费者:消费/处理东西
  • 该模型还需要一个媒介

【2】场景引入

  • 比如做包子是先将包子做好后放在蒸笼(媒介)里面,买包子的去蒸笼里面拿
  • 厨师做菜之后用盘子(媒介)装着给消费者端过去
  • 生产者与消费者之间不是直接做交互的,而是借助于媒介
  • 生产者(做包子的) + 媒介(蒸包子) + 消费者(吃包子的)

【3】场景实现

(1)消费者大于生产者

  • 消费者没有数据被卡主
from multiprocessing import Process, Queue
import time
import random


def producer(name, food, q):
    '''

    :param name: 生产者名字
    :param food: 生产的数据:食物
    :param q: 队列
    :return:
    '''
    for i in range(1, 4):
        data = f'当前大厨:>>> [{name}] 生产出:>>>[{i}道{food}]菜'
        # 模拟延迟
        time.sleep(random.randint(1, 3))
        q.put(data)


def customer(name, q):
    '''

    :param name: 消费者名字
    :param q: 队列
    :return:
    '''
    # 假定消费者消费能力很强
    while True:
        food = q.get()
        # 模拟延迟
        time.sleep(random.randint(1, 3))
        print(f'当前消费者:>>>[{name}] 消费了:>>>[{food}]')


def main():
    pass


if __name__ == '__main__':
    # (1)建立模型(媒介,蒸笼)
    que_ue = Queue()
    # (2) 建立模型(生产者,厨师)
    p_producer_dream = Process(target=producer, args=('dream', '鱼香肉丝', que_ue))
    p_producer_sprout = Process(target=producer, args=('sprout', '宫保鸡丁', que_ue))
    # (3) 建立模型(消费者,顾客)
    p_customer_smile = Process(target=customer, args=('smile', que_ue))
    p_customer_sad = Process(target=customer, args=('sad', que_ue))

    # (3)启动
    p_producer_dream.start()
    p_producer_sprout.start()
    p_customer_smile.start()
    p_customer_sad.start()

    # (4)等待
    p_producer_dream.join()
    p_producer_sprout.join()
    p_customer_smile.join()
    p_customer_sad.join()

    '''
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [dream] 生产出:>>>[1道鱼香肉丝]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [dream] 生产出:>>>[2道鱼香肉丝]菜]
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [sprout] 生产出:>>>[1道宫保鸡丁]菜]
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [sprout] 生产出:>>>[2道宫保鸡丁]菜]
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [sprout] 生产出:>>>[3道宫保鸡丁]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [dream] 生产出:>>>[3道鱼香肉丝]菜]
    '''

(2)为生产者添加结束标志

  • 向队列的结尾添加结束标志的符号,作为结束的标志
  • 但是消费者的个数决定了添加标志的个数
from multiprocessing import Process, Queue, JoinableQueue
import time
import random


def producer(name, food, q):
    '''

    :param name: 生产者名字
    :param food: 生产的数据:食物
    :param q: 队列
    :return:
    '''
    for i in range(1, 4):
        data = f'当前大厨:>>> [{name}] 生产出的:>>>[{i}道{food}]菜'
        # 模拟延迟
        time.sleep(random.randint(1, 3))
        q.put(data)


def customer(name, q):
    '''

    :param name: 消费者名字
    :param q: 队列
    :return:
    '''
    # 假定消费者消费能力很强
    while True:
        food = q.get()

        # 对收到的信息进行判断:是否有结束的标志
        if food is None:
            print('消费者消费完毕:>>>!!')
            break

        # 模拟延迟
        time.sleep(random.randint(1, 3))
        print(f'当前消费者:>>>[{name}] 消费了:>>>[{food}]')


def main():
    pass


if __name__ == '__main__':
    # (1)建立模型(媒介,蒸笼)
    que_ue = Queue()
    # (2) 建立模型(生产者,厨师)
    p_producer_dream = Process(target=producer, args=('dream', '鱼香肉丝', que_ue))
    p_producer_sprout = Process(target=producer, args=('sprout', '宫保鸡丁', que_ue))
    # (3) 建立模型(消费者,顾客)
    p_customer_smile = Process(target=customer, args=('smile', que_ue))
    p_customer_sad = Process(target=customer, args=('sad', que_ue))

    p_producer_dream.start()
    p_producer_sprout.start()
    p_customer_smile.start()
    p_customer_sad.start()

    p_producer_dream.join()
    p_producer_sprout.join()
    # 等待生产者消费完毕之后,向队列中添加特定的结束符号
    que_ue.put(None)  # 本条数据一定是生产者将数据全部生产完之后添加进去的
    que_ue.put(None)  # 本条数据一定是生产者将数据全部生产完之后添加进去的

    '''
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [dream] 生产出的:>>>[1道鱼香肉丝]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [sprout] 生产出的:>>>[1道宫保鸡丁]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [dream] 生产出的:>>>[2道鱼香肉丝]菜]
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [sprout] 生产出的:>>>[2道宫保鸡丁]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [sprout] 生产出的:>>>[3道宫保鸡丁]菜]
    消费者消费完毕:>>>!!
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [dream] 生产出的:>>>[3道鱼香肉丝]菜]
    消费者消费完毕:>>>!!
    '''

(3)JoinableQueue模块

  • 每当向队列对象中存入数据的时候,队列对象内部会有一个计数器 +1
  • 每当调用一次 task_done() 方法的时候,队列对象内部的计数器 -1
  • join() 当计数器为 0 时,继续执行代码
from multiprocessing import Process, Queue, JoinableQueue
import time
import random


def producer(name, food, q):
    '''

    :param name: 生产者名字
    :param food: 生产的数据:食物
    :param q: 队列
    :return:
    '''
    for i in range(1, 4):
        data = f'当前大厨:>>> [{name}] 生产出的:>>>[{i}道{food}]菜'
        # 模拟延迟
        time.sleep(random.randint(1, 3))
        q.put(data)
    # 生产者生产数据结束,添加结束标志
    q.join()


def customer(name, q):
    '''

    :param name: 消费者名字
    :param q: 队列
    :return:
    '''
    # 假定消费者消费能力很强
    while True:
        food = q.get()

        # 模拟延迟
        time.sleep(random.randint(1, 3))
        print(f'当前消费者:>>>[{name}] 消费了:>>>[{food}]')

        # 告诉队列已经从队列中已经取出一个数据了
        q.task_done()


def main():
    pass


if __name__ == '__main__':
    # (1)建立模型(媒介,蒸笼)
    que_ue = JoinableQueue()
    # (2) 建立模型(生产者,厨师)
    p_producer_dream = Process(target=producer, args=('dream', '鱼香肉丝', que_ue))
    p_producer_sprout = Process(target=producer, args=('sprout', '宫保鸡丁', que_ue))
    # (3) 建立模型(消费者,顾客)
    p_customer_smile = Process(target=customer, args=('smile', que_ue))
    p_customer_sad = Process(target=customer, args=('sad', que_ue))

    p_producer_dream.start()
    p_producer_sprout.start()

    # 将消费者设置成守护进程:主进程死亡,子进程跟着死亡
    p_customer_smile.daemon = True
    p_customer_sad.daemon = True

    p_customer_smile.start()
    p_customer_sad.start()

    p_producer_dream.join()
    p_producer_sprout.join()

    # 等待消息队列中的所有数据被取完再往下执行代码
    que_ue.join()
    # join() 当计数器为 0 时,消费者就没有存在的必要了

    '''
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [dream] 生产出的:>>>[1道鱼香肉丝]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [sprout] 生产出的:>>>[1道宫保鸡丁]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [dream] 生产出的:>>>[2道鱼香肉丝]菜]
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [sprout] 生产出的:>>>[2道宫保鸡丁]菜]
    当前消费者:>>>[sad] 消费了:>>>[当前大厨:>>> [sprout] 生产出的:>>>[3道宫保鸡丁]菜]
    消费者消费完毕:>>>!!
    当前消费者:>>>[smile] 消费了:>>>[当前大厨:>>> [dream] 生产出的:>>>[3道鱼香肉丝]菜]
    消费者消费完毕:>>>!!
    '''

【补充】JoinableQueue模块

【1】介绍

  • JoinableQueue([maxsize]):这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。
  • 通知进程是使用共享的信号和条件变量来实现的。

【2】使用

  • JoinableQueue([maxsize])
    • maxsize是队列中允许最大项数,省略则无大小限制。
from multiprocessing import JoinableQueue
    
q = JoinableQueue()

# 方法介绍:JoinableQueue的实例p除了与Queue对象相同的方法之外还具有:

# q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常

# q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
posted @ 2024-01-23 14:24  Chimengmeng  阅读(35)  评论(0编辑  收藏  举报