31 python 多线程与多进程2

一、多进程概念

多线程就像是在一个办公室里让多个员工同时工作,但由于 Python 的全局解释器锁(GIL),多个线程在同一时间实际上只能有一个线程执行 Python 字节码,这在处理 CPU 密集型任务时会受到限制。而多进程则是开启多个独立的 “办公室”(进程),每个进程都有自己独立的 Python 解释器和内存空间,能真正实现并行计算,适合处理 CPU 密集型任务。

二、基础语法

Python的os模块提供了fork()函数。由于Windows系统没有fork()调用,因此要实现跨平台的多进程编程,可以使用multiprocessing模块的Process来创建子进程,而且该模块还提供了更高级的封装,例如批量启动进程的进程池(Pool、用于进程间通信的队列(Queue)和管道(Pipe

import multiprocessing


def 进程任务(任务编号):
    print(f"进程 {任务编号} 开始执行")
    import time
    time.sleep(2)
    print(f"进程 {任务编号} 执行完成")


if __name__ == '__main__':
    进程列表 = []
    for i in range(3):
        进程 = multiprocessing.Process(target=进程任务, args=(i,))
        进程列表.append(进程)
        进程.start()

    for 进程 in 进程列表:
        进程.join()

    print("所有进程执行完成")

代码解释

  1. 定义任务函数进程任务函数代表每个进程要执行的任务,使用time.sleep模拟耗时操作。
  2. 创建进程对象:通过multiprocessing.Process创建进程对象,指定要执行的任务函数和参数。
  3. 启动进程:调用start()方法启动进程。
  4. 等待进程结束:使用join()方法确保主进程等待所有子进程执行完毕后再继续执行后续代码。需要注意的是,在 Windows 和一些其他操作系统中,多进程代码必须放在if __name__ == '__main__':语句块中,以避免递归创建进程。

三、进程间通信(IPC)

在多个 进程并行工作时,有时需要进行信息传递,这就涉及到进程间通信(IPC)。Python 的multiprocessing模块提供了多种方式实现 IPC,如Queue(队列)和Pipe(管道)。

3.1 使用Queue进行进程间通信

import multiprocessing


def 生产者(队列):
    for i in range(5):
        print(f"生产者生产了 {i}")
        队列.put(i)


def 消费者(队列):
    while True:
        数据 = 队列.get()
        if 数据 is None:
            break
        print(f"消费者消费了 {数据}")


if __name__ == '__main__':
    队列 = multiprocessing.Queue()
    生产者进程 = multiprocessing.Process(target=生产者, args=(队列,))
    消费者进程 = multiprocessing.Process(target=消费者, args=(队列,))

    生产者进程.start()
    消费者进程.start()

    生产者进程.join()
    队列.put(None)
    消费者进程.join()

    print("所有任务完成")

代码解释

  1. 定义生产者和消费者函数生产者函数将数据放入队列,消费者函数从队列中取出数据进行处理。
  2. 创建队列和进程:使用multiprocessing.Queue创建队列,创建生产者和消费者进程,并将队列作为参数传递给它们。
  3. 启动进程:调用start()方法启动进程。
  4. 结束进程:生产者进程结束后,向队列中放入None作为结束标志,消费者进程接收到None后退出循环,最后等待所有进程结束。

3.2 使用Pipe进行进程间通信

Pipe 可以创建一对连接对象,这对对象分别代表管道的两端,在不同的进程中可以通过这两端进行数据的发送和接收

import multiprocessing


def sender(conn):
    """
    发送数据的进程函数
    :param conn: 管道的发送端
    """
    messages = ["Hello", "World", "Python"]
    for message in messages:
        print(f"发送消息: {message}")
        conn.send(message)
        # 模拟发送数据的耗时操作
        import time
        time.sleep(1)
    conn.close()


def receiver(conn):
    """
    接收数据的进程函数
    :param conn: 管道的接收端
    """
    while True:
        try:
            message = conn.recv()
            if not message:
                break
            print(f"接收到消息: {message}")
        except EOFError:
            break
    conn.close()


if __name__ == '__main__':
    # 创建管道,返回两个连接对象
    parent_conn, child_conn = multiprocessing.Pipe()

    # 创建发送进程和接收进程
    p1 = multiprocessing.Process(target=sender, args=(child_conn,))
    p2 = multiprocessing.Process(target=receiver, args=(parent_conn,))

    # 启动进程
    p1.start()
    p2.start()

    # 等待进程结束
    p1.join()
    p2.join()

    print("所有进程执行完毕")

代码解释

  1. 导入模块:导入 multiprocessing 模块,它提供了多进程编程的功能。
  2. 定义发送函数 sender
    • 该函数接收一个管道连接对象 conn 作为参数。
    • 定义一个消息列表 messages,并遍历该列表,依次将消息发送到管道中。
    • 使用 conn.send(message) 方法发送消息,time.sleep(1) 模拟发送数据的耗时操作。
    • 最后关闭连接 conn.close()
  3. 定义接收函数 receiver
    • 该函数也接收一个管道连接对象 conn 作为参数。
    • 使用 while True 循环不断从管道中接收消息,使用 conn.recv() 方法接收消息。
    • 如果接收到的消息为空,则跳出循环;如果遇到 EOFError 异常,也跳出循环。
    • 最后关闭连接 conn.close()
  4. 主程序
    • 使用 multiprocessing.Pipe() 创建一个管道,返回两个连接对象 parent_conn 和 child_conn
    • 创建两个进程 p1 和 p2,分别执行 sender 和 receiver 函数,并将相应的管道连接对象作为参数传递给它们。
    • 启动这两个进程,使用 start() 方法。
    • 等待两个进程执行完毕,使用 join() 方法。
    • 最后打印 “所有进程执行完毕”。

四、进程间同步

在多进程编程中,当多个进程需要访问共享资源时,为了避免数据不一致或其他并发问题,就需要进行进程间同步。Python 的 multiprocessing 模块提供了多种进程间同步的机制

4.1 锁(Lock

原理

锁是一种最基本的同步机制,同一时间只允许一个进程获取锁并访问共享资源,其他进程需要等待锁被释放后才能获取锁。这样可以确保在任何时刻只有一个进程对共享资源进行操作,避免数据竞争。

错误使用共享资源的示例代码

import multiprocessing

# 共享资源
shared_variable = 0
# 创建锁对象
lock = multiprocessing.Lock()


def increment():
    global shared_variable
    for _ in range(100000):
        # 获取锁
        lock.acquire()
        try:
            shared_variable += 1
        finally:
            # 释放锁
            lock.release()


if __name__ == '__main__':
    processes = []
    for _ in range(2):
        p = multiprocessing.Process(target=increment)
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print(f"最终结果: {shared_variable}")

 该示例运行的结果为最终结果: 0,,为啥会这样子?,如果我们按照前面线程的方式修改一下,变成这样:

import multiprocessing
import threading

# 共享资源
shared_variable = 0
# 创建锁对象
lock = multiprocessing.Lock()


def increment():
    global shared_variable
    for _ in range(100000):
        # 获取锁
        lock.acquire()
        try:
            shared_variable += 1
        finally:
            # 释放锁
            lock.release()


if __name__ == '__main__':
    processes = []
    for _ in range(2):
        # p = multiprocessing.Process(target=increment)
        p = threading.Thread(target=increment)
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print(f"最终结果: {shared_variable}")

结果就对了。为什么?

原因:线程会共享父进程的共享空间,所以就可以;而进程是有自己独立空间的。
在 Python 的 multiprocessing 模块中,每个子进程都有自己独立的内存空间,父进程中的全局变量 shared_variable 并不会被子进程直接共享。子进程对 shared_variable 的修改只会影响其自身的内存副本,而不会影响父进程或其他子进程的副本。

我们可以使用multiprocessing 提供的共享内存机制(如 Value 或 Array):

import multiprocessing

# 共享资源
# 使用共享内存
shared_variable = multiprocessing.Value('i', 0)  # 'i' 表示整数类型,初始值为 0
# 创建锁对象
lock = multiprocessing.Lock()


def increment(shared_variable, lock):
    for _ in range(100000):
        # 获取锁
        lock.acquire()
        try:
            shared_variable.value += 1
        finally:
            # 释放锁
            lock.release()


if __name__ == '__main__':
    processes = []
    for _ in range(2):
        p = multiprocessing.Process(target=increment, args=(shared_variable,lock))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

    print(f"最终结果: {shared_variable.value}")

注意:锁对象lock和共享变量是在全局作用域创建的,如果不传进去给子进程,就会导致子进程可能各自拥有自己的锁和共享变量,无法正确同步对共享变量的访问。
所以要将锁对象lock和共享变量作为参数传递给子进程。

代码解释

  • lock = multiprocessing.Lock():创建一个锁对象。
  • lock.acquire():获取锁,如果锁已经被其他进程获取,当前进程会阻塞等待。
  • lock.release():释放锁,允许其他进程获取锁。
  • try...finally 语句确保即使在操作过程中出现异常,锁也能被正确释放,避免死锁。

4.2 信号量(Semaphore

原理

信号量是一种更灵活的同步机制,它可以控制同时访问共享资源的进程数量。信号量内部维护一个计数器,当进程请求访问资源时,计数器减 1;当进程释放资源时,计数器加 1。当计数器为 0 时,其他进程需要等待。

import multiprocessing
import time

# 信号量,允许同时有 2 个进程访问
semaphore = multiprocessing.Semaphore(2) # 可以试试修改为1,方便观察程序运行


def worker(process_id, semaphore):
    print(f"进程 {process_id} 正在等待信号量")
    # 获取信号量
    semaphore.acquire()
    try:
        print(f"进程 {process_id} 已获取信号量,开始工作")
        time.sleep(2)
    finally:
        # 释放信号量
        print(f"进程 {process_id} 释放信号量")
        semaphore.release()



if __name__ == '__main__':
    processes = []
    for i in range(5):
        p = multiprocessing.Process(target=worker, args=(i,semaphore))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

代码解释

  • semaphore = multiprocessing.Semaphore(2):创建一个信号量对象,初始值为 2,表示允许同时有 2 个进程访问共享资源。
  • semaphore.acquire():获取信号量,如果信号量计数器大于 0,则计数器减 1,进程继续执行;否则,进程阻塞等待。
  • semaphore.release():释放信号量,计数器加 1。

 4.3 事件(Event

原理

事件是一种简单的同步机制,它可以让一个进程等待另一个进程发出的信号。事件对象内部有一个标志位,进程可以通过 set() 方法将标志位设置为 True,通过 clear() 方法将标志位设置为 False,其他进程可以通过 wait() 方法等待标志位变为 True

import multiprocessing
import time

# 创建事件对象
event = multiprocessing.Event()


def waiter(event):
    print("等待事件被设置...")
    # 等待事件被设置
    event.wait()
    print("事件已被设置,开始工作")


def setter(event):
    print("正在执行一些操作...")
    time.sleep(2)
    # 设置事件
    event.set()
    print("事件已设置")


if __name__ == '__main__':
    p1 = multiprocessing.Process(target=waiter, args=(event,))
    p2 = multiprocessing.Process(target=setter, args=(event,))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

代码解释

  • event = multiprocessing.Event():创建一个事件对象。
  • event.wait():等待事件的标志位变为 True,如果标志位已经是 True,则立即返回;否则,进程阻塞等待。
  • event.set():将事件的标志位设置为 True,唤醒所有等待该事件的进程。
  • event.clear():将事件的标志位设置为 False

 4.4 条件变量(Condition

原理

条件变量是一种更高级的同步机制,它结合了锁和事件的功能。条件变量允许进程在满足特定条件时进行等待,当条件满足时,其他进程可以唤醒等待的进程。

import multiprocessing
import time

# 创建条件变量
condition = multiprocessing.Condition()
# 共享资源
shared_variable = multiprocessing.Value('i', 0)  # 'i' 表示整数类型,初始值为 0


def producer(shared_variable, condition):
    with condition:
        print("生产者开始生产...")
        time.sleep(2)
        shared_variable.value = 1
        print("生产者生产完成,通知消费者")
        # 通知等待的消费者
        condition.notify()


def consumer(shared_variable, condition):
    with condition:
        print("消费者等待生产完成...")
        # 等待生产者生产完成
        condition.wait()
        print(f"消费者开始消费,共享变量的值为: {shared_variable.value}")


if __name__ == '__main__':
    p1 = multiprocessing.Process(target=producer, args=(shared_variable, condition))
    p2 = multiprocessing.Process(target=consumer, args=(shared_variable, condition))

    p2.start()
    p1.start()

    p1.join()
    p2.join()

代码解释

  • condition = multiprocessing.Condition():创建一个条件变量对象。
  • condition.wait():释放锁并阻塞当前进程,直到其他进程调用 condition.notify() 或 condition.notify_all() 唤醒它。
  • condition.notify():唤醒一个等待在该条件变量上的进程。
  • condition.notify_all():唤醒所有等待在该条件变量上的进程。
  • with condition:使用上下文管理器来自动获取和释放锁。

五、进程池

使用multiprocessing.Pool来创建进程池

import multiprocessing


def 处理任务(任务编号):
    print(f"开始处理任务 {任务编号}")
    import time
    time.sleep(2)
    print(f"任务 {任务编号} 处理完成")


if __name__ == '__main__':
    with multiprocessing.Pool(processes=3) as 进程池:
        任务列表 = [1, 2, 3, 4, 5]
        进程池.map(处理任务, 任务列表)

    print("所有任务完成")

代码解释

  1. 创建进程池:使用multiprocessing.Pool创建一个包含 3 个进程的进程池,with语句会在代码块结束时自动关闭进程池。
  2. 提交任务到进程池:使用map()方法将任务列表中的每个任务分配给进程池中的进程进行处理。
  3. 等待任务完成map()方法会阻塞主进程,直到所有任务都处理完成。

六、多线程与多进程的对比

6.1 适用场景

  1. 多线程:适用于 I/O 密集型任务,如网络请求、文件读写等。因为在这些任务中,线程大部分时间都在等待 I/O 操作完成,此时可以让其他线程继续执行,充分利用 CPU 时间。
  2. 多进程:适用于 CPU 密集型任务,如大规模数据计算、图像处理等。由于每个进程都有自己独立的 Python 解释器和内存空间,能真正实现并行计算,充分利用多核 CPU 的性能。

6.2 优缺点

多线程:

  • 优点:线程创建和销毁的开销较小,线程间通信和数据共享比较方便,适合处理 I/O 密集型任务时提高程序的响应性。
  • 缺点:受 Python 全局解释器锁(GIL)的限制,多个线程在同一时间只能有一个线程执行 Python 字节码,在处理 CPU 密集型任务时效率不高;线程同步和资源管理比较复杂,容易出现死锁等问题。

多进程

  • 优点:能真正实现并行计算,充分利用多核 CPU 的性能,适合处理 CPU 密集型任务;每个进程都有自己独立的内存空间,避免了多线程中的资源竞争问题。
  • 缺点:进程创建和销毁的开销较大,进程间通信和数据共享相对复杂,需要使用专门的 IPC 机制。

七、应用场景示例

import multiprocessing
import threading
import time
import requests


# 模拟文件内容提取(CPU 密集型操作)
def extract_content(file_path):
    print(f"开始提取文件 {file_path} 的内容")
    # 模拟耗时的内容提取操作
    time.sleep(2)
    content = f"文件 {file_path} 的提取内容"
    print(f"完成文件 {file_path} 的内容提取")
    return content


# 模拟网络请求(I/O 密集型操作)
def send_request(content):
    print(f"开始发送内容:{content} 到服务器")
    try:
        # 模拟网络请求
        response = requests.get("https://www.example.com")
        if response.status_code == 200:
            print(f"内容 {content} 发送成功")
        else:
            print(f"内容 {content} 发送失败,状态码:{response.status_code}")
    except Exception as e:
        print(f"内容 {content} 发送时出现错误:{e}")


# 处理单个文件的函数,结合多线程发送内容
def process_file(file_path):
    content = extract_content(file_path)
    # 创建多个线程发送内容
    threads = []
    for _ in range(3):
        thread = threading.Thread(target=send_request, args=(content,))
        threads.append(thread)
        thread.start()
    # 等待所有线程完成
    for thread in threads:
        thread.join()


if __name__ == '__main__':
    # 模拟多个文件
    file_paths = [f"file_{i}.txt" for i in range(3)]
    # 创建多个进程处理不同的文件
    processes = []
    for file_path in file_paths:
        process = multiprocessing.Process(target=process_file, args=(file_path,))
        processes.append(process)
        process.start()
    # 等待所有进程完成
    for process in processes:
        process.join()
    print("所有文件处理完成")
    

代码解释

  1. extract_content 函数:模拟文件内容提取的 CPU 密集型操作,使用 time.sleep(2) 模拟耗时操作。
  2. send_request 函数:模拟网络请求的 I/O 密集型操作,使用 requests.get 发送请求,并根据响应状态码判断请求是否成功。
  3. process_file 函数:对单个文件进行处理,先调用 extract_content 函数提取文件内容,然后创建多个线程并行发送提取的内容。
  4. 主程序
    • 定义一个包含多个文件路径的列表 file_paths
    • 创建多个进程,每个进程处理一个文件,调用 process_file 函数。
    • 启动所有进程,并等待它们完成。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爬呀爬的水滴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值