【问题标题】:How does threading save time?线程如何节省时间?
【发布时间】:2013-06-24 15:35:20
【问题描述】:

我正在学习 C# 中的线程。但是,我无法理解线程的哪些方面实际上在提高性能。

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

【问题讨论】:

  • 线程在多核环境中效率最高
  • 考虑另一种情况,仍然在单核处理器上:您正在从网站下载一个大文件。该操作很可能需要一段时间并且不会完全使用 CPU。线程允许您将空闲的 CPU 时间用于其他任务。
  • 这样想,如果你必须测试'X'的东西,你自己能不能做得更快?或者如果你和其他几个人都这样做会更快吗?
  • 您的应用程序可能正在等待 CPU 以外的其他东西 - 例如通过网络传输的数据,或从硬盘加载文件。在这种情况下,将这种情况发生在另一个线程中而不是阻塞应用程序中的主线程可能是有意义的。
  • “由于线程只是共享时间,为什么它们的运行时间(周转时间)小于单线程进程”?周转时间更长。拥有更多线程可以让您在另一个线程正在休眠等待通知唤醒时进行工作(例如您执行同步 IO 时的情况),但是通过拥有更多线程,您正在为您的操作系统/运行时创建额外的工作。

标签: c# multithreading process


【解决方案1】:

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

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

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

或者:

  1. 开始烧水。
  2. 在水沸腾时清洗/准备一些蔬菜。
  3. 在开水的锅中加入一些面条。
  4. 一边煮面条一边炒蔬菜。
  5. 装盘上桌。

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

这里的总体思路是,在编程的许多情况下,您会执行需要一些时间的操作,但它不需要 CPU 的工作即可完成。一个常见的例子是 IO。当您向数据库发送请求以获取一些信息时,在等待该请求返回时通常会有其他事情要做。也许您可以发送多个请求,然后等待它们完成,而不是启动一个,等待它,然后启动下一个,等待,依此类推(尽管有时您必须执行后者)。

现在,如果您需要做的工作是 CPU 密集型工作,那么只有在您的 CPU 上有多个内核时,您才能真正从线程中获益,这样工作实际上可以并行完成,而不仅仅是异步完成.例如,许多与图形相关的工作(矩阵相乘,举个简单的例子)通常涉及做大量的基本数学运算。如果您有多个核心,这些操作通常可以很好地扩展。如果您没有多个内核(或 GPU,实际上是一个具有 很多 个非常小而简单的内核的 CPU),那么使用线程就没有多大意义了。

【讨论】:

  • 我要补充一点,线程是实现异步的常用方法(尽管成本相对较高),因为它们往往比替代方法(回调)简单得多。有些语言试图缩小这种差异,例如 C# 5.0 或 Go。
  • @Servy- 很好的例子,如果你有一个单独的炉灶并且并行执行不依赖于炉子的任务,它会更好地解释场景。虽然你已经说清楚了,非常感谢。
【解决方案2】:

假设只有一个核心处理器存在。将您的任务分成多个线程使用相同的进程上下文(共享资源)并且它们同时运行。由于线程只是共享时间,为什么它们的运行时间(周转时间)小于单线程进程?

您对此处声称的任何加速表示怀疑是完全正确的。

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

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

  • 完成工作 1 的 100% 工作。假设这需要 1000 毫秒。
  • 完成工作 2 的 100% 工作。假设这需要 1000 毫秒。

总时间:两秒。完成的工作总数:两个。但重要的一点是:等待作业 1 的客户端只需一秒钟即可完成工作。等待作业 2 的客户端必须等待两秒钟。

现在,如果我们有两个线程和一个 CPU,它会变成这样:

  • 在 100 毫秒内完成工作 1 的 10%。
  • 完成作业 2 的 10% 的工作,持续 100 毫秒。
  • 完成工作 1 的 10%
  • 完成工作 2 的 10% ...

同样,总时间为 2 秒,但这次等待作业 1 的客户端在 1.9 秒内完成了工作,比单线程方案慢了近 100%!

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

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

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

Task Parallel Library 等库就是为此场景设计的;他们试图弄清楚何时添加更多线程会使事情变得更糟,并尝试只调度与为它们服务的 CPU 一样多的线程。

现在,如果这些条件中的任何一个得到满足,那么添加更多线程是个好主意。

  • 如果作业不受 CPU 限制,则添加更多线程允许 CPU 在空闲时执行工作,等待网络或磁盘。

  • 如果有空闲 CPU,则添加更多线程可以调度这些 CPU。

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

【讨论】:

    【解决方案3】:

    您拥有的大多数 cmets 都是正确的,但我也会扔掉我的两分钱(并在此处列出 cmets):

    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 现在必须花费更多时间在进程中的线程之间切换(请记住,我们一次只能运行一件事!)。这称为开销。第二个线程产生的开销超出了它所能弥补的范围,因此整个进程变慢了。

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

    当然,您有在集群上执行的多进程程序,但我们将把它留到下一次。

    【讨论】:

    • RAM 读取在这里可以被视为 I/O 吗?据我所知,它足够快,因此在等待读取时进行上下文切换是没有意义的(可能除了超线程,但你说忽略它)。
    • @svick:典型的消费类 RAM 的运行时间是 7-10 毫秒?在这段时间内可能会发生很多循环。老实说,我不知道上下文切换是否发生,但我会怀疑。根据askldjd.wordpress.com/2011/02/20/length-of-a-thread-quantum ,该特定cpu 的时间量约为5 毫秒......这是1300 万个cpu 周期。鉴于这些数字,我会假设 RAM 算作 IO,因为您可以放弃多达 2 个或更多完整的量子来获得一个变量。
    • 我以为DRAM operates on the order of tens of nanoseconds。几十毫秒是 HDD 的延迟。
    • @svick:抱歉,你是对的。即便如此,您仍然会损失 13000 个周期。在这种情况下,我不知道这是否被认为是可接受的损失,因此算作 I/O
    【解决方案4】:

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

    示例 1:线程使情况变得更糟

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

    如果我们连续安排这些(不使用多线程),那么在 10 分钟内我们将得到一个计算的结果,再过 10 分钟,我们将得到另一个计算的结果。

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

    示例 2:改进的线程

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

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

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

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

    对纯 CPU 密集型任务进行时间片处理有助于避免病态情况,即一项非常大的作业将其他所有工作延迟完成该大型作业所需的全部时间。

    【讨论】:

      【解决方案5】:

      请务必注意,线程本身不会使进程更快 - 有时,竞争同一个进程会增加必要的运行时间而不是减少它。一个很好的评估是首先你想要的场景是否会从多线程中受益。

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

      但是,即使对于单核系统,也存在多线程可以提高性能的实例。当一个进程正在等待某事时,它不会锁定任何其他串联运行的进程。根据等待时间的长短,您的单核可以在其他独立进程之间跳转,总体上可以节省时间。

      【讨论】:

        【解决方案6】:

        您完全正确,在单核 CPU 上使用多个线程不会提高 总 CPU 时间。事实上,由于price of context switching,它可能会使情况变得更糟。

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

        流畅的用户界面

        线程也是实现异步的一种方式,这对于流畅的用户界面尤为重要。例如,如果您执行昂贵的 CPU 密集型处理并在同一线程上处理 UI,您的程序将出现(从用户的角度)暂时“挂起”,直到处理完成。但是,如果您将处理推送到后台线程,UI 线程可以继续响应用户的输入和/或继续通知用户进度。

        非 CPU 绑定处理

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

        TPL

        在 C# 的情况下,您可能希望使用 Task Parallel Library 进行并发(以及使用 进行异步),而不是尝试自己管理低级线程。默认情况下,Tasks 将被调度到线程池中,避免线程过多(和上下文切换)的危险。

        查看Parallel Programming with Microsoft .NET 了解更多信息。

        【讨论】:

          猜你喜欢
          • 2011-10-12
          • 2015-11-22
          • 1970-01-01
          • 1970-01-01
          • 2013-06-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-06-27
          相关资源
          最近更新 更多