python进程和线程(二、主要讲解进程)

Python 多进程(multiprocessing)是绕过 GIL(全局解释器锁)、实现真正并行计算的核心方案,专门解决CPU 密集型任务(如大量计算、数据处理)的效率问题。
 

一、为什么需要多进程?

  1. GIL 限制:CPython 的 GIL 让同一时刻只有一个线程执行 Python 字节码,多线程无法利用多核 CPU。
  2. 多进程优势
    • 每个进程有独立的 Python 解释器和内存空间,真正并行,充分利用多核 CPU
    • 进程间互不干扰,稳定性更高
     
  3. 适用场景
     
    ✅ CPU 密集型任务(科学计算、视频编码、数据分析)
     
    ❌ 不适合 IO 密集型(文件 / 网络请求,用多线程 / 协程更轻量)

二、创建进程的主要方式

Python 标准库multiprocessing是多进程编程的核心,提供完整的进程创建、通信、同步、管理功能。

1. 基础:创建进程

方式 1:Process 类(最常用)

import multiprocessing
import time

# 定义任务函数
def task(name):
    print(f"进程 {name} 开始执行")
    time.sleep(2)  # 模拟任务
    print(f"进程 {name} 执行完成")

if __name__ == '__main__':
    # 1. 创建进程对象
    p1 = multiprocessing.Process(target=task, args=("进程1",))
    p2 = multiprocessing.Process(target=task, args=("进程2",))

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

    # 3. 等待进程执行完毕(主进程阻塞)
    p1.join()
    p2.join()

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

image

这是最直接和灵活的方式,适用于创建少量、自定义逻辑的进程。

方式 2:继承 Process 类

适合复杂逻辑,封装进程功能:
from multiprocessing import Process

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

    # 进程执行的核心逻辑
    def run(self):
        print(f"子进程 {self.name} 运行中")

if __name__ == '__main__':
    p = MyProcess("测试进程")
    p.start()
    p.join()

image

2. 使用进程池 (Pool)进行创建

与多线程(线程池)类似,手动创建大量进程会浪费资源,进程池可以复用进程、控制最大并发数,是生产环境首选。

  • pool.map(func, iterable): 将可迭代对象中的每个元素分配给一个进程处理,并按顺序返回结果。

  2.1. 基础用法(with版本)

from multiprocessing import Pool
import time


def task(n):
    time.sleep(1)
    return n * n


if __name__ == '__main__':
    # 创建进程池,默认使用CPU核心数
    with Pool(processes=4) as pool:
        # 批量提交任务
        results = pool.map(task, [1, 2, 3, 4, 5])

    print("结果:", results)   # 结果的类型是列表

image

  2.2. 基础用法(非with版本)

如果不使用 with 语句(上下文管理器)来管理 multiprocessing.Pool,你需要手动控制进程池的生命周期
这意味着你必须显式地调用 close() 和 join() 方法,否则可能会导致子进程变成“僵尸进程”或者主程序在任务完成前就意外退出。
以下是标准的写法模板和关键注意事项:

🛠️ 标准写法模板

核心原则是:先关闭(不再接受新任务),再等待(确保任务执行完毕)。
from multiprocessing import Pool
import time

def worker(n):
    time.sleep(1)
    return n * n

if __name__ == '__main__':
    pool = None
    try:
        # 1. 创建进程池
        pool = Pool(processes=4)

        # 2. 提交任务 (例如 map 或 apply_async)
        results = pool.map(worker, range(10))

        # 3. 获取结果 (如果是 apply_async,记得调用 .get())
        print(results)

    except Exception as e:
        print(f"发生错误: {e}")
    finally:
        # 4. 资源清理 (至关重要!)
        if pool is not None:
            pool.close()  # 告诉Pool不再接收新任务
            pool.join()  # 阻塞主进程,等待所有工作进程退出
            print("进程池已安全关闭")

image

  2.3. 使用 concurrent.futures.ProcessPoolExecutor

  这是一个更高级、更现代的接口,用法与 ThreadPoolExecutor 高度一致,使得在进程和进程之间切换变得非常简单。
from concurrent.futures import ProcessPoolExecutor
import time

# CPU 密集型任务:模拟计算
def heavy_calculation(n):
    total = 0
    for i in range(n):
        total += i
    return total

if __name__ == '__main__':  # Windows/macOS 必须加这个入口判断
    start = time.time()
    
    # 创建进程池,max_workers=进程数(默认=CPU核心数)
    with ProcessPoolExecutor(max_workers=4) as executor:
        # 提交任务,返回结果列表
        results = executor.map(heavy_calculation, [10**7, 10**7, 10**7, 10**7])
    
    print(f"计算结果:{list(results)}")
    print(f"耗时:{time.time() - start:.2f}s")

image

concurrent.futures.ProcessPoolExecutor常用方法

executor.map()(批量执行,按顺序返回结果)

最常用,适合任务参数相同、需要按顺序拿结果的场景。
# 函数 + 可迭代参数列表 → 自动分配给进程池执行
results = executor.map(func, [arg1, arg2, arg3])
executor.submit()(单个提交,灵活控制)
 
适合需要单独获取每个任务状态 / 结果的场景:
future = executor.submit(heavy_calculation, 10**7)
print(future.result())  # 获取任务返回值
as_completed()(按完成顺序获取结果)
 
谁先跑完就先拿谁的结果,不等待全部完成:
from concurrent.futures import ProcessPoolExecutor, as_completed

if __name__ == '__main__':
    with ProcessPoolExecutor(4) as executor:
        futures = [executor.submit(heavy_calculation, 10**7) for _ in range(4)]
        
        for future in as_completed(futures):
            print(f"任务完成,结果:{future.result()}")

3. 异步非阻塞(推荐)

apply_async 是 Python multiprocessing.Pool 类中的一个核心方法,用于异步地将单个任务提交到进程池中执行。
它的最大特点是非阻塞:调用后主进程不会等待该任务完成,而是立即继续执行后续代码,从而实现并行处理,这对于提升 CPU 密集型任务的效率至关重要。

⚙️ 语法与参数

pool.apply_async(func, args=(), kwds={}, callback=None, error_callback=None)
  • func: 子进程中要执行的函数。
  • args: 传递给 func 的位置参数,需要一个元组(即使只有一个参数)。
  • kwds: 传递给 func 的关键字参数,需要一个字典。
  • callback: (可选) 一个回调函数,当任务成功执行后会自动调用,任务的返回值会作为参数传给它。
  • error_callback: (可选) 一个错误回调函数,当任务执行出错时会自动调用,异常对象会作为参数传给它。
该方法会立即返回一个 AsyncResult 对象,你可以使用它来获取任务的最终结果。
from multiprocessing import Pool

def task(x):
    return x**2

if __name__ == '__main__':
    pool = Pool(4)
    # 异步提交,不阻塞主进程
    # 返回一个AsyncResult对象
    result = pool.apply_async(task, args=(5,))
    # 获取结果
    print(result.get())
    pool.close()
    pool.join()

image

下面再列举一个“异步非阻塞”代码案例:

import multiprocessing
import time


# 模拟一个耗时的计算任务
def square_number(n):
    time.sleep(1)  # 模拟耗时操作
    return n * n


# 任务成功的回调函数
def on_success(result):
    print(f"回调函数收到结果: {result}")


# 任务失败的回调函数
def on_error(error):
    print(f"任务出错: {error}")


if __name__ == '__main__':
    # 创建一个包含4个工作进程的进程池
    with multiprocessing.Pool(processes=4) as pool:
        results = []

        # 异步提交多个任务
        for i in range(5):
            async_result = pool.apply_async(
                square_number,
                args=(i,),
                callback=on_success,
                error_callback=on_error
            )
            results.append(async_result)
            print(f"主进程:任务 {i} 已提交")

        print("主进程:所有任务已提交,正在等待...")

        # 关闭进程池,不再接受新任务
        pool.close()
        # 等待所有工作进程完成
        pool.join()

        # 通过 AsyncResult 对象获取每个任务的结果
        # .get() 方法是阻塞的,直到结果就绪
        final_results = [res.get() for res in results]
        print(f"最终结果: {final_results}")

image

以上输出结果中,高亮的部分是在执行时瞬间输出的。

我们再来写一个“同步阻塞”的代码案例,做为对比:

import multiprocessing
import time


# 模拟一个耗时的计算任务 (保持不变)
def square_number(n):
    time.sleep(5)  # 模拟耗时操作
    return n * n


if __name__ == '__main__':
    # 创建一个包含4个工作进程的进程池
    with multiprocessing.Pool(processes=4) as pool:
        final_results = []

        print("主进程:开始同步提交任务...")
        start_time = time.time()  # 记录开始时间

        # --- 核心改动区域 ---
        for i in range(5):
            # 【阻塞】这里会卡住!主进程必须等待当前任务完成才能继续下一次循环
            result = pool.apply(square_number, args=(i,))

            # 只有当上面的任务做完,拿到了 result,才会执行到这里
            final_results.append(result)
            print(f"主进程:任务 {i} 已完成,结果为 {result}")
        # --- 核心改动区域 ---

        end_time = time.time()
        print("-" * 30)
        print(f"主进程:所有任务已串行完成")
        print(f"最终结果列表: {final_results}")
        print(f"总耗时: {end_time - start_time:.2f} 秒")

 image

以上代码的输出情况为:第一行输出“主进程:开始同步提交任务...”,5s等待后,输出“主进程:任务 0 已完成,结果为 0”,又5s等待后输出......,以此类推。

三、进程之间数据共享

Python 中多进程multiprocessing)的内存是完全独立的,默认无法直接共享数据,必须通过专门的机制实现共享。
 
我按简单易用 → 高性能的顺序,给你整理了最常用、工业级可用的 4 种方案,直接复制就能用。

1. 最简单:Value / Array(共享单一值 / 数组)

适合共享数字、简单数组,底层是共享内存,效率最高。
from multiprocessing import Process, Value, Array

def func(n, a):
    n.value = 100      # 修改共享数字
    a[0] = 999         # 修改共享数组

if __name__ == '__main__':
    # 共享整数(默认线程/进程安全)
    num = Value('i', 0)
    # 共享整型数组
    arr = Array('i', [1,2,3])

    p = Process(target=func, args=(num, arr))
    p.start()
    p.join()

    print(num.value)   # 输出 100
    print(arr[:])     # 输出 [999, 2, 3]

image

适用场景:少量基础数据(int/float/ 数组)。

2. 最常用:Manager(共享 dict/list/Namespace)

Manager最通用的方案,可以共享:dict, list, Value, Namespace 等几乎所有对象。
 
由一个独立的服务进程管理数据,跨进程、跨机器都能用
from multiprocessing import Process, Manager

def func(d, l):
    d['name'] = 'test'
    d['age'] = 20
    l.append(100)

if __name__ == '__main__':
    with Manager() as manager:
        # 创建共享字典
        shared_dict = manager.dict()
        # 创建共享列表
        shared_list = manager.list()

        p = Process(target=func, args=(shared_dict, shared_list))
        p.start()
        p.join()

        print(shared_dict)  # {'name': 'test', 'age': 20}
        print(shared_list)  # [100]
image
优点:简单、通用、支持复杂结构。
 
缺点:性能比共享内存稍低。

3. 高性能:Queue / Pipe(消息队列)

适合进程间通信、传递数据自带进程安全,不用手动加锁。

Queue(队列,多进程安全)

from multiprocessing import Process, Queue

def worker(q):
    q.put("Hello from child process!")  # 子进程向队列放入数据

if __name__ == "__main__":
    q = Queue()
    p = Process(target=worker, args=(q,))
    p.start()
    print(q.get())  # 主进程从队列获取数据,输出: "Hello from child process!"
    p.join()

image 

🔗 管道 (Pipe)

管道提供了一种更直接的双向通信通道,通常用于两个进程之间的通信。
  • 特点:性能比 Queue 更高,但只适合一对一通信。需要注意避免死锁(例如,两个进程同时调用 recv())。
  • 适用场景:两个进程之间需要进行高效、实时的双向数据交换。
from multiprocessing import Process, Pipe

# 子进程要执行的函数
def worker(conn):
    conn.send("消息A")  # 子进程 → 主进程:发送消息A
    print(conn.recv())  # 子进程阻塞等待:接收主进程的消息

if __name__ == "__main__":
    # 创建管道,得到两端连接:父(主)进程端、子进程端
    parent_conn, child_conn = Pipe()
    
    # 创建子进程,把 child_conn 传给子进程
    p = Process(target=worker, args=(child_conn,))
    p.start()  # 启动子进程
    
    # 主进程阻塞等待:接收子进程的消息A
    print(parent_conn.recv())
    
    # 主进程 → 子进程:发送消息B
    parent_conn.send("消息B")
    
    p.join()  # 等待子进程执行完毕

核心概念

  • Pipe():创建一个双向通信管道,返回两个连接对象 (parent_conn, child_conn)
  • 两个进程分别持有一端,通过 send() 发消息、recv() 收消息
  • recv()阻塞方法:没收到消息会一直等待,直到对方发送数据

完整执行流程(时序图)

1. 主进程创建管道 → 得到 parent_conn、child_conn
2. 主进程创建并启动子进程
3. 子进程执行 worker:
   → 发送 "消息A" 给主进程
   → 调用 recv() 阻塞等待主进程回复
4. 主进程执行 recv():
   → 收到 "消息A" 并打印
5. 主进程发送 "消息B" 给子进程
6. 子进程收到 "消息B" 并打印
7. 子进程结束,主进程 join 等待完成

 

4. 最高性能:SharedMemory(Python 3.8+ 大内存共享)

专门用于大数据共享(如:图片、数组、大列表、 Pandas 数据),直接共享内存块,性能远超 Manager。

 

from multiprocessing import Process
from multiprocessing.shared_memory import SharedMemory
import numpy as np

def write_shm():
    # 创建共享内存
    shm = SharedMemory(create=True, size=100)
    # 转成 numpy 数组
    arr = np.ndarray((25,), dtype='int32', buffer=shm.buf)
    arr[0] = 999
    return shm.name

def read_shm(name):
    shm = SharedMemory(name=name)
    arr = np.ndarray((25,), dtype='int32', buffer=shm.buf)
    print(arr[0])  # 999
    shm.close()

if __name__ == '__main__':
    shm = write_shm()
    p = Process(target=read_shm, args=(shm.name,))
    p.start()
    p.join()
    shm.unlink()  # 释放共享内存

 

四、进程锁

进程锁是多进程编程中解决资源竞争问题的核心工具,用于保证同一时间只有一个进程访问共享资源(文件、数据库、屏幕输出、共享变量等),避免数据错乱、输出混乱。
 

核心知识点

  1. 适用场景multiprocessing 多进程(线程锁是 threading.Lock,二者不通用)
  2. 作用:加锁 → 独占资源执行 → 释放锁,实现原子操作
  3. 原理:锁是进程间共享的,必须通过进程传参传递

基础用法(标准模板)

import multiprocessing
import time

# 共享函数:多个进程会同时调用
def task(lock, num):
    # 加锁:阻塞其他进程,直到锁被释放
    lock.acquire()
    try:
        # 临界区:同一时间只有一个进程执行这里
        print(f"进程 {num} 开始执行")
        time.sleep(1)
        print(f"进程 {num} 执行完毕\n")
    finally:
        # 释放锁:必须释放,否则会造成死锁
        lock.release()

if __name__ == '__main__':
    # 创建进程锁(全局唯一)
    process_lock = multiprocessing.Lock()
    
    # 创建多个进程
    processes = []
    for i in range(3):
        p = multiprocessing.Process(target=task, args=(process_lock, i))
        processes.append(p)
        p.start()
    
    # 等待所有进程结束
    for p in processes:
        p.join()

 image

 

简化写法(推荐:with 语句)

with自动加锁 + 自动释放锁,无需手动写 acquire/release,彻底避免死锁:

 

import multiprocessing
import time

# 共享函数:多个进程会同时调用
def task(lock, num):
    # with 自动管理锁
    with lock:
        print(f"进程 {num} 开始执行")
        time.sleep(1)
        print(f"进程 {num} 执行完毕\n")

if __name__ == '__main__':
    # 创建进程锁(全局唯一)
    process_lock = multiprocessing.Lock()

    # 创建多个进程
    processes = []
    for i in range(3):
        p = multiprocessing.Process(target=task, args=(process_lock, i))
        processes.append(p)
        p.start()

    # 等待所有进程结束
    for p in processes:
        p.join()

image

为什么需要进程锁?(对比演示)

无锁:输出混乱(资源竞争)

多个进程同时打印,文字会错乱穿插,代码案例如下:
import multiprocessing
import time

# 共享函数:多个进程会同时调用
def task(num):
    # with 自动管理锁
    # with lock:
        print(f"进程 {num} 开始执行")
        time.sleep(1)
        print(f"进程 {num} 执行完毕\n")

if __name__ == '__main__':
    # 创建进程锁(全局唯一)
    # process_lock = multiprocessing.Lock()

    # 创建多个进程
    processes = []
    for i in range(3):
        p = multiprocessing.Process(target=task, args=(i,))
        processes.append(p)
        p.start()

    # 等待所有进程结束
    for p in processes:
        p.join()

image

 

实战场景:多进程写入同一文件

这是进程锁最常用的场景,无锁会导致文件内容错乱、覆盖。
import multiprocessing

# 写入文件的函数
def write_file(content):
    # 加锁,保证独占写入
    # with lock:
        with open("test.txt", "a", encoding="utf-8") as f:
            f.write(content + "\n")


if __name__ == '__main__':
    # lock = multiprocessing.Lock()
    p1 = multiprocessing.Process(target=write_file, args=("进程1写入数据",))
    p2 = multiprocessing.Process(target=write_file, args=("进程2写入数据",))

    p1.start()
    p2.start()
    p1.join()
    p2.join()

image

从输出结果来看,test.txt文件中只出现了“进程1写入数据”。

原因:

当多个进程同时向同一个文件写入数据时,它们可能会相互干扰。例如,一个进程还没写完,另一个进程就开始写,这会导致写入的内容交错、覆盖或损坏。
multiprocessing.Lock() 就像一个“通行证”。在任何时刻,只有一个进程能拿到这个通行证(获取锁),执行写入操作。其他进程必须等待,直到这个进程完成任务并归还通行证(释放锁)。
 
import multiprocessing


# 写入文件的函数
def write_file(lock, content):
    # 加锁,保证独占写入
    with lock:
        with open("test.txt", "a", encoding="utf-8") as f:
            f.write(content + "\n")


if __name__ == '__main__':
    lock = multiprocessing.Lock()
    p1 = multiprocessing.Process(target=write_file, args=(lock, "进程1写入数据"))
    p2 = multiprocessing.Process(target=write_file, args=(lock, "进程2写入数据"))

    p1.start()
    p2.start()
    p1.join()
    p2.join()

image

上面是加了锁的情况,“进程1写入数据”和“进程2写入数据”都可以正常写入文件中。

 

 

🔁 可重入锁 (RLock)

标准的 Lock 在同一进程内不能被重复获取,否则会导致死锁。RLock (Reentrant Lock) 解决了这个问题,它允许同一个进程多次获取它自己持有的锁。这在递归函数或一个函数内部调用另一个同样需要该锁的函数时非常有用。

 

from multiprocessing import Process, RLock

def recursive_func(rlock, depth):
    with rlock:  # 同一进程可以多次获取RLock
        if depth > 0:
            print(f"进程 {depth} 正在执行...")
            recursive_func(rlock, depth - 1)

if __name__ == '__main__':
    rlock = RLock()
    p = Process(target=recursive_func, args=(rlock, 3))
    p.start()
    p.join()

image 

 

 

🚦 信号量 (Semaphore)

信号量是锁的升级版,它维护一个内部的计数器,用于控制可以同时访问某个资源的进程数量。例如,你可以用它来限制最多只有2个进程能同时访问一个数据库连接池。

 

from multiprocessing import Process, Semaphore
import time

def access_resource(semaphore, process_id):
    with semaphore:  # 获取信号量,计数器减1
        print(f"进程 {process_id} 正在访问资源...")
        time.sleep(2)  # 模拟耗时操作
        print(f"进程 {process_id} 完成访问。")
    # 离开 with 块时自动释放信号量,计数器加1

if __name__ == '__main__':
    # 创建一个信号量,允许最多2个进程并发访问
    sem = Semaphore(2)
    processes = [Process(target=access_resource, args=(sem, i)) for i in range(5)]
    
    for p in processes:
        p.start()
    for p in processes:
        p.join()

image 

从以上输出结果来看,始终保持2个进程访问,一个进程访问结束,会马上增加另一个进程,但是始终保持不大于2个进程的访问。

 

 

 

 

posted @ 2026-04-30 20:18  chenlight  阅读(5)  评论(0)    收藏  举报