何时在C#中使用线程池?

127

我一直在尝试学习C#中的多线程编程,但我对于何时最好使用线程池而不是创建自己的线程感到困惑。有一本书建议仅针对小任务使用线程池(无论这意味着什么),但我似乎找不到任何实际的指南。

线程池和创建自己的线程各有优缺点。它们的使用场景有哪些?

15个回答

50

我建议你在C#中使用线程池,原因与任何其他语言都相同。

当你想限制正在运行的线程数量或不想创建和销毁它们时,请使用线程池。

所谓小任务是指具有短寿命的任务。如果创建一个只运行一秒钟的线程需要十秒钟,那么这就是你应该使用线程池的地方(忽略我的实际数字,重要的是比值)。

否则,你会花费大量时间来创建和销毁线程,而不是简单地完成它们旨在完成的工作。


49

如果您有很多需要不断处理的逻辑任务,并且希望以并行方式完成,可以使用池+调度程序。

如果您需要同时进行与IO相关的任务,例如从远程服务器或磁盘访问下载内容,但每隔几分钟需要执行一次,则可以自己创建线程并在完成后终止它们。

编辑:关于一些考虑因素,我使用线程池进行数据库访问、物理/模拟、AI(游戏)以及在处理大量用户定义任务的虚拟机上运行的脚本任务。

通常,池由每个处理器的2个线程组成(现在可能是4个),但如果您知道需要多少线程,则可以设置所需的线程数。

编辑:自己创建线程的原因是因为上下文更改(当线程需要进入和退出进程时,以及它们的内存)。如果有无用的上下文更改,比如当您不使用线程时,只是让它们坐着,这很容易使程序的性能减半(假设您有3个休眠线程和2个活动线程)。 因此,如果那些下载线程只是在等待,它们会消耗大量CPU资源并降低缓存对您真正应用程序的影响。


2
好的,但你能解释一下为什么要这样做吗?比如,使用线程池从远程服务下载或进行磁盘IO的缺点是什么? - creedence.myopenid.com
10
如果一个线程正在等待同步对象(事件、信号量、互斥锁等),那么线程不会占用CPU。 - Brannon
7
正如布莱农所说的,一个常见的谬论是创建多个线程会影响性能。实际上,未使用的线程消耗非常少的资源。只有在极高需求的服务器中,上下文切换才会成为问题(在这种情况下,请参考I/O完成端口以获取替代方案)。 - F.D.Castel
14
空闲线程是否会影响性能?这要看它们等待的方式。如果编写良好并且等待同步对象,则它们不应该占用CPU资源。如果在循环中等待并定期唤醒以检查结果,则会浪费CPU资源。正如往常一样,这归结于良好的编码习惯。 - Bill
2
空闲的托管线程会占用堆栈内存。默认情况下,每个线程为1 MiB。因此最好让所有线程都在工作状态。 - Vadym Stetsiak
显示剩余3条评论

28

8
-1是针对链接的评价。我相信这是一个好的链接,但我希望SO能够自给自足。 - Jon Davis
26
@stimpy77 - 那是错误的期望。SO永远不可能自给自足,因为它既不是所有问题的最终权威,也不应该在每个涉及该主题的SO答案中复制和包含所有深入信息。(而且我认为您甚至没有足够的声誉来单独对Jon Skeet的带有外部链接的每个答案进行投票,更不用说对所有带有外部链接的SO用户的答案进行投票了 :-)) - Franci Penov
2
也许我过于简洁了,或许我应该澄清一下。我不反对链接,我反对只包含链接的答案。我认为那不是一个答案。如果对链接内容进行了简短的总结以概括其适用性,那么这将是可以接受的。此外,我来到这里是为了寻找同样问题的答案,而这个答案让我感到恼火,因为它又是一个我必须点击才能知道它与特定问题相关的内容。无论如何,Jon Skeet与此有什么关系?我为什么要在意呢? - Jon Davis
9
你在这篇帖子发布两年后才来到这里,我复制这里的任何内容现在可能已经过时了。链接也可能是如此。当发布链接时,请提供简洁但完整的摘要,因为你永远不知道链接是否会失效或无效。 - Jon Davis
2
我不同意stimpy的观点:不是因为不可行性而包含大量信息的帖子的想法,也不是因为这个问题而指责某人。我认为链接失效的可能性比内容过时/被废弃的可能性更大。因此,在适当的情况下,更多的内容是好的。我们都是(大多数)志愿者,所以要感激你所得到的 - 谢谢Franci :) - zanlok
显示剩余3条评论

14

我强烈推荐阅读这本免费电子书:《C#多线程编程》作者是Joseph Albahari。

至少阅读“入门”部分。这本电子书提供了一份很棒的介绍,并包括大量高级多线程信息。

判断是否使用线程池只是开始。接下来,您需要确定哪种进入线程池的方法最适合您的需求:

  • 任务并行库(.NET Framework 4.0)
  • ThreadPool.QueueUserWorkItem
  • 异步委托
  • BackgroundWorker

这本电子书解释了这些并建议何时使用它们相比创建自己的线程。


8
线程池旨在减少线程间的上下文切换。考虑一个运行多个组件的进程,每个组件都可能创建工作线程。进程中线程越多,浪费在上下文切换上的时间也就越多。
如果这些组件将任务排队到线程池中,那么上下文切换开销就会减少很多。
线程池旨在最大化跨 CPU(或 CPU 核心)执行的工作量。这就是为什么默认情况下线程池会为每个处理器启动多个线程。
有一些情况下你不应该使用线程池。如果你正在等待 I/O 或事件等,则会占用线程池线程,其他人无法使用它。对于长时间运行的任务也是同样的道理,尽管长时间运行的任务的定义因人而异。
Pax Diablo 也提出了一个很好的观点。启动线程并不是免费的。它需要时间,并且会消耗额外的内存用于堆栈空间。线程池将重复使用线程以分摊这个成本。
注意:你问及是否可以使用线程池线程来下载数据或执行磁盘 I/O。你不应该使用线程池线程进行此操作(出于我上面列出的原因)。而应该使用异步 I/O(又称 BeginXX 和 EndXX 方法)。对于 FileStream,应该使用 BeginReadEndRead。对于 HttpWebRequest,应该使用 BeginGetResponseEndGetResponse。它们使用起来更加复杂,但是这是执行多线程 I/O 的正确方式。

1
ThreadPool是一个聪明的自动化工具。"如果它的队列静止超过半秒钟,它会响应并创建更多的线程-每半秒钟一个-直到线程池的容量达到上限"(http://www.albahari.com/threading/#_Optimizing_the_Thread_Pool)。同时,几乎所有使用BeginXXX-EndXXX进行异步操作的方法都是通过ThreadPool实现的。因此,使用ThreadPool下载数据是很正常的,并且经常被隐式使用。 - Artur A

6

当进行任何显著、可变或未知处理时间的操作时,要注意.NET线程池可能会出现线程饥饿的情况。考虑使用.NET并行扩展,它们提供了许多逻辑抽象来处理线程操作。它们还包括一个新的调度器,应该比线程池更好。请参阅此处


2
我们是通过艰苦的实践发现的!ASP.Net使用线程池,因此我们无法像我们想要的那样积极地使用它。 - noocyte

3

仅将线程池用于小任务的一个原因是,线程池线程数量有限。如果一个线程长时间被占用,则其他代码无法使用该线程。如果这种情况发生多次,则线程池可能会被用尽。

用尽线程池可能会产生微妙的影响-例如,一些.NET计时器使用线程池线程并且不会启动。


2
为了实现最高效的并发执行单元,您可以编写自己的线程池。在启动时创建一组线程对象,并使其进入阻塞状态(以前是挂起状态),等待运行上下文(由您的代码实现的具有标准接口的对象)。

有关任务、线程和.NET线程池的许多文章未能真正提供您需要的性能决策所需的信息。但是当您进行比较时,线程胜出,尤其是线程池。它们可以在CPU之间最好地分配,并且启动更快。

应该讨论的问题是Windows(包括Windows 10)的主要执行单元是线程,操作系统上下文切换开销通常可以忽略不计。简而言之,我无法找到这些文章中很多声称通过节省上下文切换或改善CPU使用率来获得更高性能的令人信服的证据。

现在稍微现实一点:

我们大多数人不需要我们的应用程序具有确定性,我们大多数人没有线程方面的艰苦背景,例如开发操作系统。我上面写的不是给初学者看的。

因此,最重要的可能是讨论易于编程的内容。

如果您创建自己的线程池,您需要进行一些编写工作,因为您需要关注跟踪执行状态、如何模拟挂起和恢复以及如何取消执行(包括在应用程序范围内关闭)。您还可能需要考虑是否要动态增长池以及池的容量限制。我可以在一个小时内编写这样的框架,但那是因为我做过很多次。

也许编写执行单元最简单的方法是使用Task。Task的优点是您可以在代码中创建一个Task并立即启动它(尽管可能需要小心)。您可以传递取消令牌来处理何时取消任务。此外,它使用承诺方法来链接事件,并且可以返回特定类型的值。此外,使用async和await,有更多选项,您的代码将更具可移植性。

总之,重要的是了解Tasks vs. Threads vs. the .NET ThreadPool的优缺点。如果需要高性能,则会使用线程,并且更喜欢使用自己的池。

比较简单的方法是启动512个线程、512个任务和512个ThreadPool线程。您将发现线程在开始时存在延迟(因此,为什么要编写线程池),但所有512个线程都将在几秒钟内运行,而任务和.NET ThreadPool线程需要多达几分钟才能全部启动。

下面是这样一个测试的结果(i5四核16 GB RAM),每个运行30秒。执行的代码在SSD驱动器上执行简单的文件I/O。

测试结果


1
顺便提一下,任务和.NET线程是在.NET中模拟并发执行的,管理执行在.NET而不是操作系统中 - 后者更有效地管理并发执行。我用任务做很多事情,但对于重度执行性能,我使用操作系统线程。微软声称任务和.NET线程更好,但它们通常用于在.NET应用程序之间平衡并发性。然而,服务器应用程序最好让操作系统处理并发性。 - user2769898
很想看到你自定义线程池的实现。写得不错! - Francis
我不理解你的测试结果。 "Units Ran" 是什么意思? 你用512个线程比较了34个任务? 你能解释一下吗? - Elmue
Unit只是在任务、线程或.NET ThreadPool工作线程中并行执行的方法,我的测试比较了启动/运行性能。每个测试都有30秒的时间从头开始生成512个线程、512个任务、512个ThreadPool工作线程或恢复一个等待上下文执行的512个已启动线程池。任务和ThreadPool工作线程需要慢速启动,因此30秒不足以将它们全部启动。但是,如果首先将ThreadPool最小工作线程计数设置为512,则Tasks和ThreadPool工作线程将几乎与从头开始生成的512个线程一样快地启动。 - user2769898
1
@Francis https://github.com/grabe/NativeWindowsThreadPool - user2769898

2
如果您有一个需要长时间运行的后台任务,例如整个应用程序的生命周期,则创建自己的线程是合理的。如果您需要在线程中完成短暂的工作,则使用线程池。
在创建许多线程的应用程序中,创建线程的开销变得相当大。使用线程池只创建一次线程并重复使用它们,从而避免了线程创建开销。
在我所工作的应用程序中,从创建线程更改为对短期线程使用线程池真正有助于提高应用程序的吞吐量。

请澄清您是指“线程池”还是“该线程池”。这些东西非常不同(至少在MS CLR中)。 - bzlm

1

不要忘记调查后台工作器。

我发现在许多情况下,它能够满足我的需求,而无需费力。

干杯。


当一个简单的应用程序保持运行并且您有另一项任务要完成时,编写此代码非常容易。不过,您没有提供链接:规范教程 - zanlok

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接