超线程
超线程技术是英特尔为其处理器架构推出的一种同时多线程技术,核心思想是让一个核心能够作为两个逻辑核心对外呈现,使得每个物理核心能够同时处理两个线程。
+------------------+
| 物理核心 (Core) |
+------------------+
/ \
/ \
+----------------+ +----------------+
| 逻辑核心/线程 1 | | 逻辑核心/线程 2 |
+----------------+ +----------------+
虽然粗略来看逻辑核心数量翻倍,但它并不会使性能直接翻倍,因为两个线程仍然共享同一核心中的大部分硬件资源。它利用的背景是现代 CPU 在处理各类指令时可能会遇到内存延迟或等待计算资源的情况,超线程允许另一个线程在等待期间执行计算,从而隐藏延迟、提高执行效率。
我们知道,一个物理核心正常情况下同一时刻只能真正处理一个线程,只是操作系统飞速“切换”不同线程,使得看起来多个线程在同时运行。而超线程技术会把物理核心会被虚拟成两个逻辑核心,通过合并空闲资源在同一时刻处理来自两个线程的指令,从而提升整体吞吐量,但逻辑核心毕竟不是物理核心,所以并不等同于两个独立物理核心完全并行工作。
正因为两个逻辑核心共享大部分硬件资源,所以当两个线程争抢同一核心内资源时,可能引入轻微的资源冲突,实际性能提升可能只有 20%-30%,而不是 100%。
Python、Java 多线程(这里只考虑 CPython)
总有人说 Python 的多线程是假的。
既然一个核心在同一时间只能处理一个线程,那如果只启动一个 Java 进程和一个 Python 进程,为什么说他们对多线程的效果不一样。一个 Java 进程,一个 Python 进程,不是也分别只能有一个线程让核心跑吗?
在单核环境下:
-
Java:Java 的线程通常由 JVM 映射到系统层面的原生线程。线程的创建、上下文切换和调度都依靠操作系统。虽然在单个核心上,操作系统每次只能让一个线程获得 CPU 执行权,但原生线程之间的切换非常高效。当某个线程因为 I/O 等原因阻塞时,操作系统可以立刻切换到另一个线程,从而充分利用时间片。
-
Python(CPython):多线程受到全局解释器锁(GIL)的限制。GIL 确保了同一时刻只有一个线程在解释器中执行 Python 字节码。即使底层操作系统调度多个线程,当涉及纯粹的 Python 代码执行时,GIL 会阻止真正的并行执行。这使得对于 CPU 密集型任务,多线程效果不明显;而对于 I/O 密集型任务(比如网络请求或文件操作),线程在等待外部操作完成时可以释放 GIL,从而允许其他线程运行。
-
CPU 密集型任务
- Java:物理上同一时刻它还是一个线程在执行,但借助 JIT 编译和高效的线程实现使得线程切换开销较小。
- Python:在频繁的 GIL 争用中耗费额外开销。
-
I/O 密集型任务
- Java:多线程加速十分明显
- 由于 GIL 对于非阻塞状态下的线程执行仍然影响计算密集部分,因此在混合负载下可能效果不如预期。
可以说,Python 与 Java 在多线程处理上的主要区别之一是 GIL 的存在,这不仅会带来额外的上下文切换开销,还会限制 Python 在 CPU 密集型任务上的并行能力。而 Java 没有这个限制,能够更高效地利用多核处理器实现并行计算。
多核处理器上的表现
在多核处理器上,如果还是只启动一个 Java 进程和一个 Python 进程。
- Java:在多核处理器上,一个 Java 进程中如果有两个或更多线程,操作系统的线程调度器会尽量将它们分派到不同的 CPU 核心上运行,从而实现真正的并行执行。
- Python:从操作系统角度来看,Python 线程和 Java 线程一样,都是原生线程,操作系统会尽量把它们分配到不同的核上。但是,由于 CPython 的全局解释器锁(GIL)的存在,即使两个线程被调度到不同的核心上,同一时刻也只有一个线程能执行 Python 字节码。
------------ ------------
| 核心 1 | | 核心 2 |
------------ ------------
| |
| |
+---------+ +---------+
| 线程 1 | | 线程 2 |
+---------+ +---------+
一个 Java 进程内的两个线程被分别调度到两个不同的核心上,从而允许并行执行
操作系统调度(不受GIL影响):
Core 1 Core 2
| |
+---------+ +---------+
| Thread A| | Thread B|
+---------+ +---------+
↑
GIL(保护下只允许一个线程运行Python字节码)
一个 Python 进程中的两个线程理论上确实可以被调度到两个不同的核上,
但由于 GIL 的存在,在进行 CPU 密集型工作时,它们不会真正实现并行执行