首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >CPU 只能看到线程

CPU 只能看到线程

原创
作者头像
timerring
修改2025-06-06 01:02:58
修改2025-06-06 01:02:58
14600
代码可运行
举报
运行总次数:0
代码可运行

本文翻译自我的英文博客,最新修订内容可随时参考:CPU can only see the threads

在 Python 中,由于 GIL(全局解释器锁)的存在——这是一个互斥锁,确保同一时刻只有一个线程能执行——因此在 CPython 解释器下不支持多线程并行执行。但多进程呢?它们之间的区别是什么?如何选择合适的方法?你了解协程吗?让我们一起探讨。

前置知识

首先,你需要了解以下基本概念:

  1. 进程(Process):进程是资源分配的基本单位。
  2. 线程(Thread):线程是 CPU 调度的最小单位。

对于每个进程,实际的执行单元是进程中的主线程。因此,即使在不同进程中,CPU 实际看到的仍然是线程

计算机的核心是可同时并行的物理核心数量(CPU 只能“看到”线程)。由于超线程技术(Hyper-Threading),实际可并行的线程数通常是物理核心数的 两倍(这也是操作系统看到的核心数)。我们只关心可并行的线程数,因此 后文提到的核心数均指操作系统看到的核心数(即包含超线程后的逻辑核心,非物理核心)。

  • 若计算机有多个 CPU 核心,且系统中线程总数少于核心数,线程可并行运行在不同核心上。
  • 若为单核多线程,多线程并非并行,而是 并发(CPU 调度器在单个核心上切换不同线程以平衡负载)。
  • 若为多核多线程且线程数超过核心数,部分线程会持续切换(并发执行),但 实际最大并行执行数等于当前核心数。因此,盲目增加线程数不会提升程序速度,反而会增加额外开销。

进程(Process)

  1. 资源分配单位:每个进程拥有独立的运行空间(包括文本段、数据段、栈段)。
  2. 独立内存空间:进程间内存地址空间隔离,确保安全性。
  3. 进程组成:包含程序、数据集和进程控制块(PCB),PCB 记录进程占用的资源信息。
  4. 调度单位:进程切换需保存和恢复 CPU 状态(如寄存器值、程序计数器等)。
  5. 进程间通信(IPC):无法直接通信,需通过 管道(父子进程)、命名管道、信号、消息队列、共享内存(效率最高)、套接字(跨机器) 等机制。

缺点

  • 进程频繁切换开销大(内存占用 GB 级别)。

线程(Thread)

  1. CPU 调度最小单位:线程是轻量化的进程,又称“轻量级进程”。
  2. 共享内存空间:同一进程内的线程共享进程资源(如内存、文件句柄),可直接通信。
  3. 调度开销小:线程切换仅涉及栈(KB 级别)和寄存器,远小于进程切换。
  4. 资源依赖:从属于进程,不独立分配资源。线程由 栈(系统栈/用户栈)、寄存器、线程控制块(TCB) 组成,寄存器仅存储本线程局部变量,无法访问其他线程数据。

其他对比

  1. 独立性
    • 进程间相互独立,一个进程崩溃不影响其他进程。
    • 线程间不独立,一个线程崩溃会导致整个进程崩溃。
  2. 内存安全
    • 进程使用可锁定的内存地址(如互斥锁),当一个线程占用共享内存时,其他线程必须等待。

选择策略

  1. CPU 密集型任务:推荐使用多进程(规避 GIL 限制,充分利用多核)。
  2. IO 密集型任务:推荐使用多线程(如网络爬虫,IO 阻塞时线程释放 GIL,允许其他线程运行)。
  3. 常见场景
    • Web 服务器(频繁创建/关闭连接):多线程更合适。
    • 数据强关联场景(如共享缓存):多线程更高效(无需跨进程通信)。

上下文切换(Context Switching)

类型
  1. 进程上下文切换:不同进程间的切换,任务调度采用 时间片轮转抢占式策略
  2. 线程上下文切换:同一进程内不同线程间的切换。
  3. 用户态与内核态切换:用户程序调用硬件设备时,需从用户态切换到内核态执行系统调用。
步骤
  1. 切换地址空间:仅进程切换需更换页表(虚拟内存空间),线程切换共享同一地址空间(因此进程切换开销最大)。
  2. 切换内核栈与硬件上下文:最耗时的是寄存器内容的保存与恢复。
性能瓶颈判断

若 CPU 满负载,合理的时间分配应为:

  • 用户态时间(User Time):65%~70%
  • 系统态时间(System Time):30%~35%(过高则说明上下文切换频繁)
  • 空闲时间(Idle):0%~5%

Python 中的多线程

其他语言中,多线程可并行运行在多个核心上,但在 Python 中,同一时刻只有一个线程能获取 CPU。

Python 的 GIL(全局解释器锁)确保同一时刻只有一个线程执行字节码。Python 有多种解释器:

  • CPython:官方实现(用 C 编写),存在 GIL。
  • Jython:将 Python 代码编译为 Java 字节码,运行于 JVM(无 GIL)。
  • IronPython:.NET 平台实现(无 GIL)。
  • PyPy:用 RPython 实现(GIL 限制较弱)。

为什么只有 CPython 有 GIL?

CPython 的内存管理(如垃圾回收)并非线程安全。若无 GIL,多线程同时操作对象时可能导致内存泄漏或程序崩溃。GIL 确保同一时刻只有一个线程访问 CPython 解释器,从而保证线程级安全。

线程安全

线程安全主要涉及内存安全。进程内所有线程共享堆内存,若无保护机制,数据可能被其他线程意外修改。常见解决方案包括互斥锁(Mutex)、信号量(Semaphore)等。

Python 线程使用示例
代码语言:python
代码运行次数:0
运行
复制
# 直接使用 threading 模块
import threading

def run(n):
    print("当前任务:", n)

if __name__ == "__main__":
    t1 = threading.Thread(target=run, args=("线程 1",))
    t2 = threading.Thread(target=run, args=("线程 2",))
    t1.start()
    t2.start()

# 自定义线程类
class MyThread(threading.Thread):
    def __init__(self, name):
        super().__init__()
        self.name = name
    def run(self):
        print("当前任务:", self.name)

# 主线程等待子线程完成
t1.start()
t2.start()
t1.join()  # 阻塞主线程,直至 t1 完成
t2.join(timeout=5)  # 带超时的等待

# 守护线程(Daemon Thread):随主线程退出而终止
t1.setDaemon(True)
t1.start()
定时器与线程局部存储
代码语言:python
代码运行次数:0
运行
复制
# 定时器:延迟执行任务
t = threading.Timer(1, show)  # 1 秒后执行 show 函数
t.start()

# 线程局部存储(每个线程独立数据)
local_school = threading.local()  # 创建线程局部变量
local_school.student = "Alice"  # 仅当前线程可见
线程同步与锁
代码语言:python
代码运行次数:0
运行
复制
# 互斥锁(Lock)
mutex = threading.Lock()
mutex.acquire()  # 加锁
# 临界区代码
mutex.release()  # 解锁

# 可重入锁(RLock):允许同一线程多次获取锁
reentrant_lock = threading.RLock()

# 信号量(Semaphore):限制同时访问资源的线程数
semaphore = threading.BoundedSemaphore(5)  # 最多 5 个线程同时获取锁

Python 中的多进程

多进程可绕过 GIL 限制,充分利用多核 CPU。

代码语言:python
代码运行次数:0
运行
复制
# 基本用法
from multiprocessing import Process

def show(name):
    print("子进程名称:", name)

if __name__ == "__main__":
    proc = Process(target=show, args=("子进程",))
    proc.start()
    proc.join()  # 等待子进程完成
进程间通信
  1. 队列(Queue): from multiprocessing import Queue def put_data(queue): queue.put("数据") # 向队列中放入数据 queue = Queue() proc = Process(target=put_data, args=(queue,)) proc.start() print(queue.get()) # 从队列中取出数据(阻塞直到有数据)
  2. 管道(Pipe): from multiprocessing import Pipe parent_conn, child_conn = Pipe() # 创建双向管道 def send_data(conn): conn.send("管道数据") conn.close() proc = Process(target=send_data, args=(child_conn,)) proc.start() print(parent_conn.recv()) # 接收数据
  3. 进程池(Pool): from multiprocessing import Pool import time def task(msg): print("任务:", msg) time.sleep(1) if __name__ == "__main__": pool = Pool(processes=3) # 最多 3 个进程并行 for i in range(5): pool.apply_async(task, args=(f"任务{i}",)) # 异步非阻塞提交任务 pool.close() # 关闭进程池,不再接受新任务 pool.join() # 等待所有任务完成

协程(Coroutine)

协程是用户态的轻量级线程,基于事件循环(Event Loop)和 await 主动切换,无需操作系统调度。

  • 特点:单线程执行,无线程切换开销,栈内存仅 KB 级别。
  • 安全性:单线程环境下无资源竞争问题(除非主动使用 await 切换)。
代码语言:python
代码运行次数:0
运行
复制
import asyncio

balance = 0

async def update_balance(n):
    global balance
    balance += n
    await asyncio.sleep(1)  # 主动让出控制权(模拟 IO 阻塞)
    balance -= n
    print(balance)

# 运行协程
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(
    update_balance(10), 
    update_balance(20)
))
print("最终余额:", balance)  # 输出:0

注意

  • 若协程中无 await,则顺序执行,结果确定。
  • 若有 await,需通过锁(如 asyncio.Lock)保证数据一致性,此时异步会退化为同步。

总结

场景

推荐方案

理由

CPU 密集型任务

多进程

规避 GIL,利用多核并行

IO 密集型任务

多线程/协程

线程在 IO 阻塞时释放 GIL,协程轻量级适合高并发 IO

超高频调度任务

协程

无内核上下文切换,调度成本极低

跨机器通信

多进程+套接字

进程隔离性强,套接字支持分布式通信

参考资料

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前置知识
  • 进程(Process)
  • 线程(Thread)
  • 其他对比
  • 选择策略
  • 上下文切换(Context Switching)
    • 类型
    • 步骤
    • 性能瓶颈判断
  • Python 中的多线程
    • 线程安全
    • Python 线程使用示例
    • 定时器与线程局部存储
    • 线程同步与锁
  • Python 中的多进程
    • 进程间通信
  • 协程(Coroutine)
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档