我正在学习 C# 中的线程。但是,我无法理解线程的哪些方面实际上在提高性能。
考虑只存在一个核心处理器的场景。将您的任务分成多个线程使用相同的进程上下文(共享资源)并且它们同时运行。由于线程只是共享时间,为什么它们的运行时间(周转时间)少于单线程进程?
我正在学习 C# 中的线程。但是,我无法理解线程的哪些方面实际上在提高性能。
考虑只存在一个核心处理器的场景。将您的任务分成多个线程使用相同的进程上下文(共享资源)并且它们同时运行。由于线程只是共享时间,为什么它们的运行时间(周转时间)少于单线程进程?
在单核 CPU 中,您获得的优势是通过异步。使用线程是实现这一目标的一种方法(尽管不是唯一的方法)。
想象一下做饭的过程。你认为哪个更快:
或者改为:
根据我的经验,第二个更快。
这里的一般想法是,在许多情况下,在编程时,您将有一个需要一些时间的操作,但它不需要 CPU 的工作来完成。一个常见的例子是 IO。当您向数据库发送请求以获取一些信息时,在等待该请求返回时通常会有其他事情需要您做。也许您可以发送多个请求,然后等待它们完成,而不是开始一个请求,等待它,然后开始下一个请求,等待,等等(尽管有时您必须执行后者)。
现在,如果您需要做的工作是 CPU 密集型工作,那么如果您的 CPU 上有多个内核,那么您实际上只能从线程中获得好处,这样工作实际上可以并行完成,而不仅仅是异步完成。例如,很多与图形相关的工作(乘以矩阵,举个简单的例子)通常涉及做很多基础数学。如果您有多个核心,这些操作通常可以很好地扩展。如果您没有多个内核(或 GPU,它实际上是一个具有许多非常小且简单的内核的 CPU),那么使用线程就没有多大意义了。
考虑只存在一个核心处理器的场景。将您的任务分成多个线程使用相同的进程上下文(共享资源)并且它们同时运行。由于线程只是共享时间,为什么它们的运行时间(周转时间)少于单线程进程?
您对这里声称的加速持怀疑态度是完全正确的。
首先,正如 Servy 和其他人在他们的回答中指出的那样,如果作业不受处理器限制,那么显然这里可能会有一些加速,因为当处理器空闲等待磁盘或网络恢复时,它可能正在做另一个线程的工作。
但是让我们假设您有两个处理器绑定任务、一个处理器以及两个线程或一个线程。在单线程场景中,它是这样的:
总时间:两秒。完成的工作总数:两个。但重要的一点 是:等待工作 1 的客户只需一秒钟即可完成工作。等待作业 2 的客户端必须等待两秒钟。
现在,如果我们有两个线程和一个 CPU,它会变成这样:
同样,总时间为 2 秒,但这次等待作业 1 的客户端在 1.9 秒内完成了工作,比单线程方案慢了近 100%!
这就是这里故事的寓意,你完全正确地指出。如果满足以下条件:
然后添加更多线程只会减慢您的速度。
Task Parallel Library 等库就是为此场景设计的;他们试图弄清楚何时添加更多线程会使事情变得更糟,并尝试只调度与 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 现在必须花费更多时间在进程中的线程之间切换(请记住,我们一次只能运行一件事!)。这称为开销。第二个线程产生的开销超出了它所能弥补的范围,因此整个过程变慢了。
在多核机器上,你有两个物理执行器……这意味着第二个线程可以使用一个全新的核心。这意味着它不必与许多其他事物竞争执行时间。因此,我们在这里得到了显着的加速。
然后当然你有在集群上执行的多进程程序,但我们将把它保存到另一个时间。
如果计算被分成并发的控制线程,这会改变周转时间。
假设我们要进行两次计算,每次需要 10 分钟。
如果我们连续安排这些(没有多线程),那么在 10 分钟内我们将得到一个计算的结果,再过 10 分钟,我们将得到另一个计算的结果。
如果我们在计算之间进行时间切片,那么我们将不得不等待 20 分钟,在此之后,我们会突然得到两个结果。
假设我们要进行两次计算。一个需要 1 分钟,另一个需要 59 分钟,但我们不知道。(请记住,我们只是一个不理解代码的调度程序。)
如果我们只是一个一个地运行这两个作业,可能会先安排 59 分钟的作业。因此,我们必须等待 59 分钟才能获得一个结果,然后再等待一分钟才能获得第二个结果。基本上一个小时的等待两个结果。
如果幸运的话,我们最终会先运行较短的作业,并在 1 分钟内获得第一个结果,59 分钟后获得第二个结果:平均周转时间要好得多。
但是假设我们在带有线程的作业之间进行时间切片。然后我们在 2 分钟内得到第一个作业的结果,58 分钟后得到第二个作业的结果。这几乎与第二种情况一样好,但无需预测哪个工作将是短期工作。
对纯 CPU 密集型任务进行时间切片的线程有助于避免病态的情况,即一个非常大的作业将其他所有事情延迟完成该大型作业所需的全部时间。
您完全正确,在单核 CPU 上使用多个线程不会提高总 CPU 时间。事实上,由于上下文切换的代价,它可能会使情况变得更糟。
但总 CPU 时间只是故事的一半......
线程也是实现异步的一种方式,这对于流畅的用户界面尤为重要。例如,如果您执行昂贵的 CPU 密集型处理并在同一线程上处理 UI,您的程序将出现(从用户的角度)暂时“挂起”,直到处理完成。但是,如果您将处理推送到后台线程,UI 线程可以继续响应用户的输入和/或继续通知用户进度。
最重要的是,并非所有处理都受 CPU 限制。如果您执行诸如读取文件、访问数据库或调用 Web 服务之类的操作,则线程将在等待外部资源时被阻塞(并且 CPU 未充分利用)。如果还有其他线程需要做一些工作,它们可以在第一个线程被阻塞时使用 CPU 周期。
在 C# 的情况下,您可能希望使用任务并行库来实现并发(并使用async-await来实现异步),而不是尝试自己管理低级线程。默认情况下,Task会被调度到线程池中,避免线程过多(以及上下文切换)的危险。
重要的是要注意线程本身不会使进程更快 - 有时,竞争相同的进程会增加必要的运行时间而不是减少它。一个很好的评估是,您想要的场景是否首先会从多线程中受益。
线程的基本要点是使用可用资源进行多任务处理——正如 KooKiz 所说,就像在可用时使用剩余的 CPU 时间一样。但你是对的,在某些情况下使用线程不会改善运行时间。
然而,即使对于单核系统,也存在多线程可以提高性能的情况。当一个进程正在等待某事时,它不会锁定任何其他串联运行的进程。根据等待的时间长短,您的单核可以在其他独立进程之间跳转,总体上可以节省时间。