线程如何节省时间

本文关键字:节省时间 线程 | 更新日期: 2023-09-27 18:19:46

我正在C#中学习线程。然而,我不明白线程的哪些方面实际上在提高性能。

考虑一个只有一个核心处理器的场景。将任务拆分为多个线程使用相同的进程上下文(共享资源),并且它们同时运行。由于线程只是共享时间,为什么它们的运行时间(周转时间)少于单个线程进程?

线程如何节省时间

在单核CPU中,通过异步可以获得优势。使用线程是实现这一点的一种方法(尽管不是唯一的方法)。

想象一下做饭的过程。你认为哪个更快:

  1. 开始煮一些水。等待它结束
  2. 加一些面条。等它们煮熟
  3. 洗/准备一些蔬菜
  4. 把蔬菜炒一下
  5. 装上盘子上桌

或者:

  1. 开始煮一些水
  2. 水开的时候洗/准备一些蔬菜
  3. 往沸水锅里加入一些面条
  4. 面条煮的时候把蔬菜炒一下
  5. 装上盘子上桌

根据我的经验,第二个更快。

这里的总体想法是,在许多情况下,当编程时,你会有一个需要一些时间的操作,但它不需要CPU完成工作。一个常见的例子是IO。当你向数据库发送请求以获取一些信息时,在等待请求返回时,通常会有其他事情要做。也许你可以发送几个请求,然后等待它们完成,而不是开始一个,等待它,然后开始下一个,等等(尽管有时你必须做后者)。

现在,如果你需要做的工作是CPU绑定的工作,那么只有当你的CPU上有多个核心,这样工作实际上可以并行完成,而不仅仅是异步完成时,你才能真正从线程中获得好处。例如,许多与图形相关的工作(举个简单的例子,乘以矩阵)通常涉及到做很多的基本数学。如果你有几个核心,这些操作通常可以很好地扩展。如果你没有多个内核(或者GPU,它实际上是一个CPU,有很多非常小和简单的内核),那么使用线程没有多大意义。

考虑一个只有一个核心处理器的场景。将任务拆分为多个线程使用相同的进程上下文(共享资源),并且它们同时运行。由于线程只是共享时间,为什么它们的运行时间(周转时间)少于单个线程进程?

你对这里任何所谓的加速持怀疑态度是完全正确的。

首先,正如Servy和其他人在他们的回答中指出的那样,如果作业不是处理器绑定的,那么这里显然会有一些加速,因为当处理器空闲等待磁盘或网络恢复时,它可能正在做另一个线程的工作

但是,让我们假设您有两个处理器绑定的任务,一个处理器,以及两个线程或一个线程。在单线程场景中,它是这样的:

  • 完成作业1的100%工作。假设这需要1000ms
  • 做100%的工作2。假设这需要1000ms

总时间:两秒。总共完成了两项工作。但重要的一点是:等待作业1的客户只需一秒钟就完成了任务正在等待作业2的客户端必须等待两秒钟。

现在,如果我们有两个线程和一个CPU,它是这样的:

  • 做作业1的10%的工作,持续100毫秒
  • 做作业2的10%的工作,持续100毫秒
  • 完成作业1的10%工作
  • 完成作业2 10%的工作

同样,总时间是两秒,但这一次等待作业1的客户端在1.9秒内完成了作业,比单线程场景慢了近100%

这就是这个故事的寓意,你完全正确地指出了这一点。如果满足以下条件:

  • 作业受CPU限制
  • 线程比CPU多
  • 这项工作只对其最终结果有用

然后添加更多线程只会减慢速度

像任务并行库这样的库就是为这个场景设计的;他们试图弄清楚什么时候添加更多的线程会让事情变得更糟,并试图只调度有多少CPU为他们服务的线程。

现在,如果其中任何一个条件不满足,那么添加更多线程是个好主意。

  • 如果作业不受CPU限制,那么添加更多的线程可以让CPU在空闲、等待网络或磁盘时进行工作。

  • 如果有空闲的CPU,那么添加更多的线程可以对这些CPU进行调度。

  • 如果部分计算的结果是有用的,那么添加更多的线程可以改善这种情况,因为客户端有更多的机会使用部分计算结果。例如,在我们的第二个场景中,两个作业的客户端每200毫秒得到一次部分结果,这是公平的

你的大多数评论都是正确的,但我也会抛出我的两分钱(并在这里列出评论):

Jonesy:"线程在多核环境中效率最高"->是的,但这是一个单核cpu。。。所以我会回到这个问题上来。

KooKiz和John Sibly:他们都提到I/O。你的机器并没有在100%的时间里全速运转。还有很多其他事情需要花费时间,在这些事件中,你的CPU会休息一下。

(参考点:I/O可以是网络传输、硬盘/RAM读取、SQL查询等。任何将新数据引入CPU或从CPU卸载数据的东西)

这些休息时间是你的cpu可以做其他事情的时间。如果你有一个单核cpu(我们现在将忽略超线程)和一个单线程应用程序,那么它可以很好地运行。CPU调度会给它一两个周期,然后转到其他事情,然后过一段时间回到你的程序,再给它几个周期,继续前进,等等。这给了在单核CPU上"一次做多件事"的错觉。

现在,由于这是一个普通的程序,而不是一些非常小的汇编程序,你可以直接将值写入缓存,所以你的程序将数据存储在RAM中。。。与CPU高速缓存相比是相对较慢的存储介质。因此,加载值需要时间。

在这段时间里,你的CPU可能没有更好的事情可做。这就是你可以在多线程应用程序上看到加速的地方,即使是在单核上。另一个线程将填充那些额外的CPU周期,否则CPU将处于空闲状态。

请注意,您不太可能看到2:1的加速。更有可能的是,你的2线程程序只会看到10-20%的速度提升,如果那样的话。记住,"另一个"线程(在任何给定点上都是不执行I/O的线程)只有在第一个线程执行I/O时才会真正满负荷运行。

然而,你经常会看到一个更糟糕的时间。这是因为您的CPU现在必须花费更多的时间在进程中的线程之间切换(记住,我们一次只能运行一件事!)。这被称为开销。第二个线程产生的开销超过了它所能弥补的,因此进程总体上会减慢。

在多核机器上,您有两个物理执行器。。。这意味着第二个线程可以使用一个全新的核心。这意味着它不必与许多其他事物竞争执行时间。因此,我们在这里得到了实质性的加速。

当然,您有在集群上执行的多进程程序,但我们将把它保存到另一个时间。

如果计算被划分为控制的并发线程,这将改变周转时间。

示例1:螺纹使情况更糟

假设我们想进行两次计算,每次需要10分钟。

如果我们连续调度这些(没有多线程),那么在10分钟内,我们将获得一次计算的结果,在另外10分钟内我们将获得另一次计算结果。

如果我们在计算之间进行时间切片,那么我们将不得不等待20分钟,在这20分钟之后,我们突然得到了两个结果。

示例2:线程使其更好

假设我们想进行两次计算。一个需要一分钟,另一个需要59分钟,但我们不知道。(记住,我们只是一个不理解代码的调度器。)

如果我们只是一个接一个地运行这两个作业,可能会先安排59分钟的作业。因此,我们必须等待59分钟才能得到一个结果,然后再等待一分钟才能得到第二个结果。基本上两个结果都要等一个小时。

如果幸运的话,我们最终会先运行较短的任务,并在1分钟内获得第一个结果,59分钟后获得第二个结果:平均周转时间要好得多。

但假设我们在具有线程的作业之间进行时间分割。然后我们在2分钟内得到第一个作业的结果,第二个作业在58分钟后得到结果。这几乎和第二种情况一样好,但不需要预测哪份工作会是短期的。

为纯粹受CPU限制的任务进行时间切片的线程处理有助于避免病态的情况,在这种情况下,一个非常大的作业会将其他一切都延迟完成该大型作业所需的全部时间

需要注意的是,线程处理本质上不会使进程更快-有时,竞争同一进程会增加而不是减少必要的运行时间。一个很好的评估是,您想要的场景是否会从多线程中受益。

线程的基本要点是使用可用资源进行多任务处理——正如KooKiz所指出的,这很像在可用时使用剩余的CPU时间。但你是对的,在某些情况下,线程的使用不会改善运行时。

然而,即使对于单核系统,也存在多线程可以提高性能的情况。当一个进程在等待某个东西时,它不会锁定任何其他同时运行的进程。根据等待的时间长短,您的单个核心可以在其他独立进程之间跳转,以节省时间。

您完全正确地认为,在单核CPU上使用多个线程不会改善总CPU时间。事实上,由于上下文切换的代价,它可能会让情况变得更糟。

但是CPU的总时间只是故事的一半。。。

Fluid UI

线程也是实现异步的一种方式,这对于流体用户界面尤为重要。例如,如果您执行昂贵的CPU绑定处理,并在同一线程上处理UI,则您的程序将(从用户的角度)显示为临时"挂起",直到处理完成。但是,如果您将处理推送到后台线程,UI线程可以不断响应用户的输入和/或不断通知用户进度。

非CPU限制处理

除此之外,并不是所有的处理都是CPU绑定的。如果您执行诸如读取文件、访问数据库或调用web服务之类的操作,那么在等待外部资源时,线程将被阻塞(并且CPU未充分利用)。如果有其他线程需要做一些工作,它们可以在第一个线程被阻塞时使用CPU周期。

TPL

在C#的情况下,您可能希望使用任务并行库来实现并发(而异步等待用于异步),而不是尝试自己管理低级线程。默认情况下,任务将在线程池上进行调度,避免了线程过多(以及上下文切换)的危险。

有关详细信息,请参阅Microsoft.NET并行编程。