1. 引言
在本教程中,我们将探讨服务器在运行时应该使用的线程数量上限问题。如何确定这个临界值?我们将先回顾一些关键概念,然后尝试提出一种合理的方法来估算理想的线程数量,从而回答这个问题:“线程数多少才算太多?”
2. 进程与线程
在计算机中,进程是正在运行的一个或多个线程的程序实例。根据操作系统的不同,一个进程可能包含多个并发执行的线程。
进程模型有很多种,有些是轻量级的,但几乎所有进程都源自操作系统中的进程模型。一个进程通常包括程序代码、分配的系统资源、物理和逻辑访问权限,以及用于启动、控制和协调执行活动的数据结构。
与静态的程序不同,进程是程序被加载到内存中执行的过程。同一个程序可以对应多个进程,比如打开多个相同软件的实例,通常会启动多个进程:
线程与传统多任务操作系统中的进程有许多不同之处。线程通常是进程的子组,而进程通常是独立的。相比线程,进程携带了更多的状态信息。一个进程的多个线程可以共享系统内存、其他资源和进程状态。
进程拥有独立的地址空间,而线程共享地址空间。进程之间的通信必须通过系统提供的进程间通信(IPC)机制完成,而线程之间通常切换上下文更快。
2.1. 线程 vs 进程:优缺点
使用线程比使用多个进程更节省资源。进程之间进行通信时,需要共享内存或消息传递机制,而线程之间可以直接通过共享的数据、代码和文件进行通信。
由于线程共享地址空间,一个线程执行非法操作可能导致整个进程崩溃,进而影响该进程中所有线程的数据处理。
一组在同一进程中运行的线程被称为线程组。正如前面所说,它们共享全局变量、文件描述符和堆内存。
这些线程可以并发执行。如果机器有多个处理器,还可以使用时间片或真正的并行处理来同时执行多个任务。这是使用线程组而非进程组的优势之一,可以实时响应事件。
上下文切换也是线程组的优势之一。在线程之间切换上下文比在进程之间切换要快得多。
2.2. 创建过多线程的问题
如果我们创建了成千上万的线程,任务执行时间反而会变长,因为上下文切换的开销会显著增加。✅建议使用线程池而不是手动创建新线程,这样操作系统可以平衡线程数量。
通常认为,物理核心数越多越好。比如,一个拥有 8 个核心和 8 个线程的 CPU 比一个拥有 2 个核心和 8 个线程的 CPU 性能更好。但线程数多有助于多任务处理,在某些高强度应用中,会同时使用多个核心。
核心数是指 CPU 芯片上的实际核心数量,而线程数则是指可同时运行的应用程序线程总数。在某些 CPU 上,线程数可能超过核心数。
2.3. 线程过多会损害性能
线程过多可能会带来两个负面影响:
- 任务被划分到太多线程上,每个线程的工作量过小,导致创建和销毁线程的开销超过实际工作量。
- 线程之间竞争有限的硬件资源,导致额外的调度和资源争用开销。
区分硬件线程与软件线程非常重要。程序创建的线程称为“软件线程”,而硬件线程是芯片上的实际物理资源。每个核心可能有 1 个或多个硬件线程。
一个合理的做法是将可运行线程数限制为硬件线程数。如果缓存争用严重,也可以限制为外层缓存的数量。避免硬编码线程数,因为目标平台的硬件线程数可能不同。✅让软件的线程数根据硬件自动调整。
虽然每个线程的内存开销不大,但线程调度器管理大量线程本身也有开销。即使部分线程处于 IO 阻塞状态,大量空闲线程仍然会增加管理负担。
3. 线程数的理想临界点
要利用并发,我们需要将问题拆分为可以并行执行的小任务。如果问题中很大一部分无法拆分,那么并发处理带来的性能提升可能有限。
如果 IO 操作延迟较高,可能需要更多线程才能达到最佳状态,因为会有更多线程处于等待状态。反之,如果延迟较低,线程释放得更快,少量线程即可达到最佳性能。
CPU 密集型代码的性能受限于 CPU 的执行速度。例如,4 个线程并发运行时,已经充分利用了 4 核 CPU 的处理能力,不会产生额外开销:
现实中,应用程序可能同时包含 CPU 密集型和 IO 密集型部分。也可能既不是 CPU 瓶颈,也不是 IO 瓶颈,而是受内存限制,或者没有充分利用资源。
这意味着每个线程都有管理开销。线程数并不是越多越好。但在某些测试中,线程数的增加带来了 100% 到 600% 的性能提升。因此,找到这个“黄金点”是非常值得的。
唯一准确的方法是通过实际测量。✅运行代码,尝试不同线程数,评估性能表现。不测量就无法得出“正确”答案。找到最佳点非常关键。经过几次实验后,我们可能可以预估合适的线程数,但这个点会因不同的 IO 负载或硬件配置而变化。
我们应该重点测量负载下并发运行的最大线程数。然后在此基础上增加 20% 作为安全余量。一旦遇到瓶颈(如 CPU、数据库、磁盘等),增加线程数就无法提升性能了。但在到达瓶颈前,可以继续增加线程。
4. 总结
本文我们讨论了服务器在运行时应使用的线程数量上限问题。可以通过在不同线程数下测量性能来确定这个临界值。要估算理想的线程数量,✅应通过实际性能测试,绘制性能曲线,找到性能峰值对应的线程数。这才是回答“线程数多少才算太多”的正确方法。