Jakob Jenkov 2021-03-09
多线程的主要优势包括:
- 更充分的 CPU 利用率
- 在某些情况下程序设计更简单
- 程序响应性更强
- CPU 资源在不同任务之间分配更公平
更充分的 CPU 利用率
设想一个从本地文件系统读取并处理文件的应用程序。假设从磁盘读取一个文件需要 5 秒,处理它需要 2 秒。那么处理两个文件将按如下顺序进行:
5 秒 —— 读取文件 A
2 秒 —— 处理文件 A
5 秒 —— 读取文件 B
2 秒 —— 处理文件 B
-----------------------
总计 14 秒
在从磁盘读取文件的过程中,CPU 大部分时间都在等待磁盘完成数据读取,几乎处于空闲状态。此时 CPU 完全可以执行其他任务。如果调整操作顺序,就能更好地利用 CPU。例如:
5 秒 —— 读取文件 A
5 秒 —— 读取文件 B
+
2 秒 —— 处理文件 A
2 秒 —— 处理文件 B
-----------------------
总计 12 秒
CPU 等待第一个文件被读取完毕后,立即开始读取第二个文件。在第二个文件由计算机的 I/O 组件读取的同时,CPU 开始处理第一个文件。记住,在等待磁盘读取文件时,CPU 基本上是空闲的。
一般来说,当 CPU 等待 I/O 操作(无论是磁盘 I/O、网络 I/O,还是用户输入)时,它可以执行其他任务。因为网络和磁盘 I/O 通常比 CPU 和内存 I/O 慢得多。
更简单的程序设计
如果你要在单线程应用程序中手动实现上述读取与处理的交错顺序,就必须同时跟踪每个文件的读取状态和处理状态。而使用多线程时,你可以启动两个线程,每个线程只负责读取并处理一个文件。这些线程在等待磁盘读取文件时会被阻塞,但在此期间,其他线程可以使用 CPU 来处理已经读入内存的部分数据。
这样做的结果是:磁盘始终处于忙碌状态,不断从多个文件中读取数据到内存;同时 CPU 也得到了更好的利用。而且程序编写也更简单,因为每个线程只需关注单个文件的状态。
更强的程序响应性
将单线程应用程序改造为多线程的另一个常见目标是提升程序的响应性。
设想一个服务器应用程序,它监听某个端口以接收客户端请求。收到请求后,它会处理该请求,然后返回继续监听。其主循环大致如下:
while (server is active) {
listen for request
process request
}
如果某个请求处理耗时很长,在此期间服务器就无法接收新的客户端请求——只有在回到监听状态时才能接收新请求。
另一种设计方式是:监听线程接收到请求后,立即将其交给工作线程处理,并马上返回继续监听。其结构如下:
while (server is active) {
listen for request
hand request to worker thread
}
这样,服务器线程能更快地回到监听状态,从而允许更多客户端发送请求,使服务器更具响应性。
桌面应用程序也是如此。如果你点击一个按钮触发一个长时间运行的任务,而执行该任务的线程同时也是负责更新窗口、按钮等 UI 元素的线程,那么在任务执行期间,应用程序界面会显得“无响应”。
解决方法是将任务交给一个工作线程去执行。这样,UI 线程就能自由响应其他用户操作。当工作线程完成任务后,再通知 UI 线程更新界面。采用这种工作线程设计的程序对用户来说会显得更加响应迅速。
更公平的 CPU 资源分配
设想一个服务器正在接收来自多个客户端的请求。其中某个客户端发送了一个处理时间很长的请求(比如 10 秒)。如果服务器使用单线程处理所有任务,那么在此之后的所有请求都必须等到这个慢请求完全处理完毕才能开始处理。
通过在多个线程之间分配 CPU 时间并进行线程切换,CPU 可以更公平地在多个请求之间共享执行时间。即使存在一个慢请求,其他处理较快的请求也可以与其并发执行。当然,这意味着慢请求的处理会变得更慢,因为它无法独占 CPU。但其他请求的等待时间会显著缩短——它们不必等到慢任务完成后才开始处理。
当然,如果系统中只有一个慢请求需要处理,CPU 仍然可以完全分配给它。