进程通信

【一】进程通信

【1】什么是进程间通信

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

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

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

【3】什么是管道

  • 管道是一种半双工的通信机制,即只能在一个方向上进行数据传输。
  • 子进程可以通过继承父进程的管道来实现通信。
  • stdin、stdout和stderr是Python中的三个内置文件对象,它们分别代表标准输入、标准输出和标准错误。
  • 子进程可以通过继承父进程的管道来实现通信。
  • 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】方法示例示例

# 导入模块
import queue

# 创建一个队列对象
q = queue.Queue(maxsize=5)

# 【1】向队列中放入东西
# put
q.put({"name":"dream"})
q.put(2)
q.put(3)
q.put(4)
print(q.full())
q.put(5)
# 【3】full 判断当前队列是否满了
print(q.full())
# 【2】从队列中取东西
# get
print(q.get())
print(q.get())
print(q.get())
print(q.get())
# 【4】检查当前队列是否为空
print(q.empty())
print(q.get())
print(q.empty())

q.put(6)
print(q.get_nowait())
# print(q.get_nowait()) # 管道中如果不存在数据,那就会立马报错
print(q.get(timeout=2))  # 如果不加 timeout 参数就会一直夯住,加了 timeout 参数,会在指定时间后抛出异常
  • 小结
# 导入模块
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】子进程与主进程之间通过队列进行通信

import time
from multiprocessing import Process, Queue


def producer(queue_obj):
    # 子进程将主进程传入的数据取出
    print(f"{queue_obj.get()}")
    queue_obj.put("这是子进程发送给主进程的消息!")


def main():
    queue_obj = Queue(5)
    task = Process(target=producer, args=(queue_obj,))
    task.start()
    queue_obj.put("这是主进程!")
    time.sleep(2)
    # 主进程无法向子进程发送消息 , 在上面发送的是空的 队列 ,要想使用子进程的前提是 子进程启动
    print(f"这是来自子进程的消息 :>>>> {queue_obj.get()}")
    task.join()


if __name__ == '__main__':
    main()

【3】子进程与子进程之间通信

from multiprocessing import Process, Queue


def producer(queue_obj):
    # 子进程将主进程传入的数据取出
    queue_obj.put("这是来自生产者的消息!")


def customer(queue_obj):
    msg = queue_obj.get()
    print(f'这是消费者取出的数据 :>>>> {msg}')


if __name__ == '__main__':
    queue_obj = Queue(5)

    # 【1】创建两个进程对象
    task_producer = Process(target=producer, args=(queue_obj,))
    task_customer = Process(target=customer, args=(queue_obj,))

    # 【2】启动子进程
    task_producer.start()
    task_customer.start()

    # 【3】等待进程结束
    task_producer.join()
    task_customer.join()

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

【1】理论

(1)生产者模型

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

(2)消费者模型

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

(3)小结

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

【2】场景

(1)消费者大于生产者

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


# 生产者函数:做包子
def producer(name, food, queue_obj):
    '''

    :param name:生产者名字
    :param food: 生产者需要生产的食物
    :param queue_obj: 队列对象
    :return:
    '''
    for i in range(1, 5):
        data = f"当前大厨 :>>>> {name} | 生产出了第 {i} 道 {food}!"
        time.sleep(random.randint(1, 3))
        # 将生产出来的数据放入媒介
        queue_obj.put(data)


def customer(name, queue_obj):
    '''

    :param name: 消费者名字
    :param queue_obj:  队列对象
    :return:
    '''
    while True:
        from_producer_food = queue_obj.get()
        time.sleep(random.randint(1, 2))
        print(f'当前消费者 :>>> {name} | 消费了 :>>>> {from_producer_food}')


def main():
    task_list = []
    # 【1】创建队列对象
    queue_obj = Queue()
    # 【2】创建消费者对象 (厨师)
    producer_dream = Process(target=producer, args=('dream', '宫保鸡丁', queue_obj))
    producer_hope = Process(target=producer, args=('hope', '鱼香肉丝', queue_obj))

    # 【3】创建消费者对象 (顾客)
    customer_opp = Process(target=customer, args=('opp', queue_obj))
    customer_happy = Process(target=customer, args=('happy', queue_obj))

    task_list.append(producer_dream)
    task_list.append(producer_hope)
    task_list.append(customer_opp)
    task_list.append(customer_happy)

    # 【4】启动
    # print(task_list)
    [task.start() for task in task_list]
    # print(task_list)
    [task.join() for task in task_list]

    # task_list_start = []
    # for task in task_list:
    #     task.start()
    #     task_list_start.append(task)
    #
    # for task_every in task_list_start:
    #     task_every.join()


if __name__ == '__main__':
    main()

(2)解决消费者大于生产者的情况

from multiprocessing import Process, Queue
import random
import time
def producer(name, food, queue_obj):
    '''

    :param name:生产者名字
    :param food: 生产者需要生产的食物
    :param queue_obj: 队列对象
    :return:
    '''
    for i in range(1, 5):
        data = f"当前大厨 :>>>> {name} | 生产出了第 {i} 道 {food}!"
        time.sleep(random.randint(1, 3))
        # 将生产出来的数据放入媒介
        queue_obj.put(data)
    # 放一个结束标志
    queue_obj.put('q')


def customer(name, queue_obj):
    '''

    :param name: 消费者名字
    :param queue_obj:  队列对象
    :return:
    '''
    while True:
        from_producer_food = queue_obj.get()
        # print(queue_obj.empty())
        if from_producer_food == 'q':
            break
        time.sleep(random.randint(1, 2))
        print(f'当前消费者 :>>> {name} | 消费了 :>>>> {from_producer_food}')


def main():
    task_list = []
    # 【1】创建队列对象
    queue_obj = Queue()
    # 【2】创建消费者对象 (厨师)
    producer_dream = Process(target=producer, args=('dream', '宫保鸡丁', queue_obj))
    producer_hope = Process(target=producer, args=('hope', '鱼香肉丝', queue_obj))

    # 【3】创建消费者对象 (顾客)
    customer_opp = Process(target=customer, args=('opp', queue_obj))
    customer_happy = Process(target=customer, args=('happy', queue_obj))

    task_list.append(producer_dream)
    task_list.append(producer_hope)
    task_list.append(customer_opp)
    task_list.append(customer_happy)

    # 【4】启动
    task_list_new = []
    for task in task_list:
        task.start()
        task_list_new.append(task)
    for task in task_list_new:
        task.join()


if __name__ == '__main__':
    # 【1】想到的解决办法一:在队列取之后判断一下当前队列是否为空
    # 不成立:因为在多进程中 empty 不成立 full 不能用
    # get_nowait()
    # 【2】给生产者添加结束标志 (解决方案)
    # 生产者生产完指定数据后,在数据结尾添加指定的结束标志
    # 当生产者拿到这个结束标志的时候就知道我生产的数据没了
    main()

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


# 生产者函数:做包子
def producer(name, food, queue_obj):
    '''

    :param name:生产者名字
    :param food: 生产者需要生产的食物
    :param queue_obj: 队列对象
    :return:
    '''
    for i in range(1, 5):
        data = f"当前大厨 :>>>> {name} | 生产出了第 {i} 道 {food}!"
        time.sleep(random.randint(1, 3))
        # 将生产出来的数据放入媒介
        queue_obj.put(data)
    queue_obj.join()


def customer(name, queue_obj):
    '''

    :param name: 消费者名字
    :param queue_obj:  队列对象
    :return:
    '''
    while True:
        from_producer_food = queue_obj.get()
        time.sleep(random.randint(1, 2))
        print(f'当前消费者 :>>> {name} | 消费了 :>>>> {from_producer_food}')

        queue_obj.task_done()
        # 会自动判断当前队列中的数据个数,如果数据个数大于1就忽略,等于0直接自动结束


def main():
    print(f'主开始')
    task_list = []
    # 【1】创建队列对象
    queue_obj = JoinableQueue()
    # 【2】创建消费者对象 (厨师)
    producer_dream = Process(target=producer, args=('dream', '宫保鸡丁', queue_obj))
    producer_hope = Process(target=producer, args=('hope', '鱼香肉丝', queue_obj))

    # 【3】创建消费者对象 (顾客)
    customer_opp = Process(target=customer, args=('opp', queue_obj))
    customer_happy = Process(target=customer, args=('happy', queue_obj))

    customer_opp.daemon = True
    customer_happy.daemon = True
    task_list.append(producer_dream)
    task_list.append(producer_hope)
    task_list.append(customer_opp)
    task_list.append(customer_happy)

    # 【4】启动
    task_list_new = []
    for task in task_list:
        task.start()
    producer_dream.join()
    producer_hope.join()
    print(f'主结束')


if __name__ == '__main__':
    # 【1】想到的解决办法一:在队列取之后判断一下当前队列是否为空
    # 不成立:因为在多进程中 empty 不成立 full 不能用
    # get_nowait()
    # 【2】给生产者添加结束标志 (解决方案)
    # 生产者生产完指定数据后,在数据结尾添加指定的结束标志
    # 当生产者拿到这个结束标志的时候就知道我生产的数据没了
    main()

生产者和消费者模型小结

# 【一】生产者和消费者模型总结
# 生产者 : 生产数据 生产完所有数据以后要加一个标志位
# 消费者 : 消费数据 从生产者生产的数据中拿出所有数据

# 【二】解决生产者和消费者不平稳一
# Queue
# 在生产者生产数据的结尾 ---> 放一个标志位
# 消费者消费数据 ---> 消费到最后一个数据的时候,得到标志位知道生产的数据空了 --> 结束了

# 【三】解决生产者和消费者不平稳二
# JoinableQueue

# 在生产者生产数据的结尾 ---> 放一个标志位 join()
# 在消费者消费数据的 ----> 正产消费 ---> 在消费数据的结尾  task_done() ---> 自动检索上面生产者是否存在 join\

【五】多线程

【1】什么是线程

  • 在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
  • 线程顾名思义,就是一条流水线工作的过程
    • 一条流水线必须属于一个车间,一个车间的工作过程是一个进程
    • 车间负责把资源整合到一起,是一个资源单位,而一个车间内至少有一个流水线
    • 流水线的工作需要电源,电源就相当于cpu
  • 所以进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
  • 多线程(即多个控制线程)的概念是在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
  • 例如
    • 北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。

【六】线程和进程的区别

  • Threads share the address space of the process that created it; processes have their own address space.
    • 线程共享创建它的进程的地址空间; 进程具有自己的地址空间。
  • Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
    • 线程可以直接访问其进程的数据段; 进程具有其父进程数据段的副本。
  • Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
    • 线程可以直接与其进程中的其他线程通信; 进程必须使用进程间通信与同级进程进行通信。
  • New threads are easily created; new processes require duplication of the parent process.
    • 新线程很容易创建; 新进程需要复制父进程。
  • Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
    • 线程可以对同一进程的线程行使相当大的控制权。 进程只能控制子进程。
  • Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
    • 对主线程的更改(取消,优先级更改等)可能会影响该进程其他线程的行为; 对父进程的更改不会影响子进程。

【七】threading模块介绍

  • multiprocess模块的完全模仿了threading模块的接口
  • 二者在使用层面,有很大的相似性,因而不再详细介绍

官网链接https://docs.python.org/3/library/threading.html?highlight=threading

【八】什么是多线程

  • 开启线程不需要在main下面执行代码,直接书写即可
  • 但是我们还是习惯性的将启动命令写在main下面
# 多线程指的是
# 在一个进程中开启多个线程
# 简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。
# 多线程共享一个进程的地址空间
# 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
# 若多个线程都是cpu密集型的,那么并不能获得性能上的增强
# 但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
# 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于Python)

【1】多线程的创建方式

# 多进程 使用 from multiprocessing import Process
# 多进程 使用 from threading import Thread
from threading import Thread
import time
import random


# 【1】方式一
def run_task(i):
    print(f'这是参数 {i}')
    time.sleep(2)


def main_first():
    task_list = []
    for i in range(1, 5):
        # 创建线程对象
        task = Thread(target=run_task, args=(i,))
        task.start()
        task_list.append(task)
    for task in task_list:
        task.join()

# 创建多线程方式二
class MyThread(Thread):
    def __init__(self, i):
        super().__init__()
        self.i = i

    def run(self) -> None:
        run_task(self.i)


def main_second():
    task_list = [MyThread(i=i) for i in range(1, 5)]
    [task.start() for task in task_list]
    [task.join() for task in task_list]

if __name__ == '__main__':
    print(f'这是主进程开始')
    start_time = time.time()
    # main_first()
    # 总耗时 :>>>>0.0019989013671875 s
    main_second()
    print(f"总耗时 :>>>>{time.time() - start_time} s")

    # 多进程 ---> 开设多个内容空间 ,让多进程跑
    # 多线程 ---> 在一个进程内开设多个线程

【2】多进程和多线程比较

# 多进程快还是多线程块
from threading import Thread
from multiprocessing import Process
import time


def work(i):
    print(f'这是参数 {i}')
    time.sleep(2)


def timer(func):
    def inner(*args, **kwargs):
        start_time = time.time()  # 0
        res = func(*args, **kwargs)
        print(f'总耗时 :>>>> {time.time() - start_time} s')
        return res

    return inner


@timer
def main_process():
    process_list = [Process(target=work, args=(i,)) for i in range(1, 5)]
    [process.start() for process in process_list]
    [process.join() for process in process_list]


@timer
def main_thread():
    thread_list = [Thread(target=work, args=(i,)) for i in range(1, 5)]
    [thread.start() for thread in thread_list]
    [thread.join() for thread in thread_list]


if __name__ == '__main__':
    # 启动多进程
    main_process()

    # 总耗时 :>>>> 2.1622650623321533 s

    # 启动多线程
    main_thread()

    # 总耗时 :>>>> 2.0023093223571777 s

    # 对比下来就是线程比进程块!

    # 从网页上有很多图片地址
    # 像图片地址发请求 ---> 拿到了图片的二进制数据

    # 保存图片数据
    # 多线程快 还是 多进程块
posted @ 2024-01-23 14:27  Fredette  阅读(64)  评论(0)    收藏  举报