上面的大多数答案都在谈论性能和同时操作。我将从不同的角度来解决这个问题。
让我们以一个简单的终端仿真程序为例。你必须做以下事情:
- 监视来自远程系统的传入字符并显示它们
- 注意来自键盘的内容并将它们发送到远程系统
(真正的终端模拟器可以做更多的事情,包括可能会将您键入的内容回显到显示器上,但我们现在将忽略它。)
现在从远程读取的循环很简单,根据以下伪代码:
while get-character-from-remote:
print-to-screen character
监听键盘和发送的循环也很简单:
while get-character-from-keyboard:
send-to-remote character
但问题是您必须同时执行此操作。如果没有线程,代码现在必须看起来更像这样:
loop:
check-for-remote-character
if remote-character-is-ready:
print-to-screen character
check-for-keyboard-entry
if keyboard-is-ready:
send-to-remote character
即使在这个故意简化的示例中,没有考虑到通信的实际复杂性,其逻辑也相当模糊。然而,使用线程,即使在单个内核上,两个伪代码循环也可以独立存在,而不会交织它们的逻辑。由于这两个线程大部分都是 I/O 密集型的,因此它们不会给 CPU 带来沉重的负担,尽管严格来说,它们比集成循环更浪费 CPU 资源。
现在,现实世界的使用当然比上面的要复杂。但是,当您向应用程序添加更多关注点时,集成循环的复杂性会呈指数级上升。逻辑变得越来越碎片化,你必须开始使用状态机、协程等技术来让事情变得易于管理。可管理,但不可读。线程使代码更具可读性。
那你为什么不使用线程呢?
好吧,如果您的任务是 CPU 密集型而不是 I/O 密集型,那么线程实际上会减慢您的系统速度。性能会受到影响。很多,在很多情况下。 (“抖动”是一个常见问题,如果您丢弃太多 CPU 绑定线程。您最终会花费更多时间更改活动线程而不是运行线程本身的内容。)此外,上述逻辑的原因之一是如此简单,以至于我特意选择了一个简单(且不切实际)的示例。如果您想在屏幕上回显输入的内容,那么当您引入共享资源的锁定时,您将获得一个新的伤害世界。只有一个共享资源这不是什么大问题,但随着您有更多资源要共享,它确实开始成为一个越来越大的问题。
所以说到底,线程是关于很多事情的。例如,正如一些人已经说过的,它是关于使 I/O 绑定的进程更具响应性(即使整体效率较低)。这也是为了让逻辑更容易理解(但前提是你最小化共享状态)。它涉及很多东西,你必须根据具体情况决定它的优点是否大于缺点。