进程 - CPU密集型王者

概述

  • 操作系统资源分配的基本单位(注意这里的 资源分配 只是分配资源)
  • 硬件多核前提下,Python multiprocessing 多进程可以实现真正并行;单进程只能并发,无法并行
  • python的多进程会 真正启动底层的多个系统进程,每个进程拥有独立的内存空间和完整的python解释器状态
  • 这完美规避了全局解释器锁(GIL)的限制,能够真正利用多核CPU

理解

多进程中的每个进程都是单线程 / 多进程中的每个进程都是多线程 哪种能规避GIL限制?
  • 结论:
    只有多进程中的每个进程都是单线程能规避
  • 解释:
    multiprocessing 开辟多个操作系统进程
    每一个进程内部,都拥有独立 Python 解释器、独立 GIL
    GIL 的生效范围:仅局限当前进程内部,进程和进程互不影响
  • 举例:
    • 场景 A:每个子进程只开 1 条线程
      • 进程内只有一个线程,不存在线程争抢 GIL。
      • 操作系统可以把多个进程调度到多核,真正并行执行 CPU 任务,充分规避 GIL 瓶颈
    • 场景 B:某一个子进程内部继续创建多条threading线程
      • ⚠️ 这个进程内部依旧受本进程自带 GIL 约束!
      • 在同一个进程里面,无论你开多少线程,同一时刻依旧只能一条线程执行 CPU 计算。
  • 认识GIL:
    • GIL 属于单个 Python 解释器实例,一个进程对应一套解释器、一把 GIL;
      多进程方案能利用多核,本质是多个独立进程可被调度到不同 CPU 核心
      如果某个进程内部使用多线程,该进程内部依旧无法并行计算,逃不开当前进程的 GIL;所以做 CPU 密集运算的最佳实践:每个工作进程保持单线程
  • 多进程适用场景:
    • 适合干那些极耗费脑力/体力的活 例如图像处理、加密解密、大规模矩阵预算计算
  • 缺点:
    • 创建进程太重了,消耗内存大

多进程演示

import multiprocessing
import time

def calculate_heavy_task(name):
    print(f"进程 {name} 开始疯狂计算...")
    time.sleep(2)  # 模拟耗时计算
    print(f"进程 {name} 计算完成!")

if __name__ == '__main__':
    start = time.time()
    # 创建两个独立的进程
    p1 = multiprocessing.Process(target=calculate_heavy_task, args=("A",))
    p2 = multiprocessing.Process(target=calculate_heavy_task, args=("B",))

    p1.start()
    p2.start()

    p1.join()
    p2.join()
    end = time.time()
    print("所有计算任务结束!", end - start)

深度剖析 - 进程的作用

线程才是真正干活的,也能调用多核心,那把“进程”这个概念完全砍掉,让所有线程直接在操作系统里裸奔,不是更轻量、更直接吗?
结论:
如果只有线程,计算机一天得崩溃一百次。线程虽然会干活,但它是个“没家产、没记性、没边界”的打工人。进程的出现,本质上是为了给线程提供“生存空间”、“保护伞”和“规矩”。

资源分配的“大管家”(给线程分家产)
  • 线程自己是几乎不拥有任何系统资源的。它没有独立的内存,不知道哪里可以存数据。
  • 进程:当你启动一个程序时,进程负责向操作系统申请大笔的“家产”——包括独立的虚拟内存空间(堆、代码段、数据段)、打开的文件句柄、网络套接字、环境变量等。
  • 线程怎么用:进程申请到这笔家产后,住在里面的所有线程共享这笔家产。如果没了进程这个大管家,线程一出生就两眼一抹黑,连把变量存在内存的哪个地方都不知道。
内存隔离的“防火墙”(防止线程互相投毒)
  • 这是有进程最关键的物理原因:安全与稳定性。
  • 如果只有线程:
    • 所有软件的线程都混在同一个内存池里。某天你写了一个有 Bug 的 C++ 线程,不小心写错了一个指针(比如野指针),直接把隔壁微信线程或者银行支付线程的内存数据给覆盖抹掉了。这不仅会导致系统极度不稳定,还带来了致命的隐私和安全隐患。
    • 有了进程之后:
      操作系统给每个进程划分了绝对隔离的虚拟内存地址空间。
      微信进程只能访问微信的内存。
      浏览器进程只能访问浏览器的内存。
      底层铁律:进程 A 的线程绝对不可能直接去读写进程 B 的内存。如果某个线程敢越界,操作系统的硬件保护机制(MMU)会当场触发“段错误(Segmentation Fault)”,直接把这个犯规的进程一枪干掉,而其他进程毫发无损。
  • 代码演示
    # 进程防火墙的例子
    import threading
    import time
    
    # ========== 全部线程共享同一进程内的全局变量(共享内存区域) ==========
    account_balance = 1000  # 银行支付线程使用
    wechat_msg_cache = []   # 微信线程使用
    
    # 线程1:银行支付线程,持续扣款
    def bank_pay_thread():
        global account_balance
        for _ in range(10):
            if account_balance >= 100:
                account_balance -= 100
                print(f"【支付线程】扣款成功,当前余额:{account_balance}")
            time.sleep(0.01)
    
    # 线程2:微信线程,持续接收消息存入缓存
    def wechat_thread():
        global wechat_msg_cache
        for i in range(10):
            wechat_msg_cache.append(f"消息{i}")
            print(f"【微信线程】存入消息,缓存长度:{len(wechat_msg_cache)}")
            time.sleep(0.01)
    
    # 线程3:有BUG的线程!无保护并发胡乱修改共享全局数据
    # 类比C++野指针乱覆盖隔壁线程内存
    def bug_thread():
        global account_balance, wechat_msg_cache
        for _ in range(8):
            # 恶意/错误篡改支付线程的余额数据
            account_balance = 0
            # 清空微信线程的消息缓存
            wechat_msg_cache.clear()
            print("【BUG线程】篡改了共享内存里的数据!")
            time.sleep(0.02)
    
    
    if __name__ == "__main__":
        t1 = threading.Thread(target=bank_pay_thread)
        t2 = threading.Thread(target=wechat_thread)
        t3 = threading.Thread(target=bug_thread)
    
        t1.start()
        t2.start()
        t3.start()
    
        t1.join()
        t2.join()
        t3.join()
    
        print("\n最终结果:")
        print(f"账户余额 = {account_balance}")
        print(f"微信消息列表 = {wechat_msg_cache}") 
    
权限与身份的“代言人”(防止流氓软件越权)
  • 操作系统在管理安全策略时,只认进程,不认线程。
    • 你在电脑上右键“以管理员身份运行”,或者在手机上弹窗提示“是否允许微信访问相册”,这个权限是赋予进程的。
    • 进程就像是一张“通行证”。有了这张通行证,住在这个进程里的所有打工线程才能合法地去读写特定文件、访问网络。如果只有线程,操作系统根本没办法精细地管理哪个线程是安全的、哪个线程是木马病毒。
软件层面的“生命周期管理”
  • 从开发和维护的角度来看,进程是软件存在的最小实体。
    • 你在任务管理器里看到的、能手动“结束任务”的,都是进程。
    • 如果一个复杂的软件(包含几百个线程)卡死了,你只需要在任务管理器里把这个进程杀掉,操作系统就会自动回收这个进程持有的所有内存、关闭它打开的所有文件,干净利落。
    • 如果只有线程,你可能得在几千个乱七八糟的线程里,苦苦寻找哪几个是属于刚才那个卡死软件的,根本无法管理。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐