我一直在尝试学习C#中的多线程编程,但我对于何时最好使用线程池而不是创建自己的线程感到困惑。有一本书建议仅针对小任务使用线程池(无论这意味着什么),但我似乎找不到任何实际的指南。
线程池和创建自己的线程各有优缺点。它们的使用场景有哪些?
我一直在尝试学习C#中的多线程编程,但我对于何时最好使用线程池而不是创建自己的线程感到困惑。有一本书建议仅针对小任务使用线程池(无论这意味着什么),但我似乎找不到任何实际的指南。
线程池和创建自己的线程各有优缺点。它们的使用场景有哪些?
我建议你在C#中使用线程池,原因与任何其他语言都相同。
当你想限制正在运行的线程数量或不想创建和销毁它们时,请使用线程池。
所谓小任务是指具有短寿命的任务。如果创建一个只运行一秒钟的线程需要十秒钟,那么这就是你应该使用线程池的地方(忽略我的实际数字,重要的是比值)。
否则,你会花费大量时间来创建和销毁线程,而不是简单地完成它们旨在完成的工作。
如果您有很多需要不断处理的逻辑任务,并且希望以并行方式完成,可以使用池+调度程序。
如果您需要同时进行与IO相关的任务,例如从远程服务器或磁盘访问下载内容,但每隔几分钟需要执行一次,则可以自己创建线程并在完成后终止它们。
编辑:关于一些考虑因素,我使用线程池进行数据库访问、物理/模拟、AI(游戏)以及在处理大量用户定义任务的虚拟机上运行的脚本任务。
通常,池由每个处理器的2个线程组成(现在可能是4个),但如果您知道需要多少线程,则可以设置所需的线程数。
编辑:自己创建线程的原因是因为上下文更改(当线程需要进入和退出进程时,以及它们的内存)。如果有无用的上下文更改,比如当您不使用线程时,只是让它们坐着,这很容易使程序的性能减半(假设您有3个休眠线程和2个活动线程)。 因此,如果那些下载线程只是在等待,它们会消耗大量CPU资源并降低缓存对您真正应用程序的影响。
这里有一个关于 .Net 中线程池的简洁概述:http://blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-a-threadpool-thread.aspx
这篇文章还介绍了一些在哪些情况下不应该使用线程池而应该启动自己的线程。
我强烈推荐阅读这本免费电子书:《C#多线程编程》作者是Joseph Albahari。
至少阅读“入门”部分。这本电子书提供了一份很棒的介绍,并包括大量高级多线程信息。
判断是否使用线程池只是开始。接下来,您需要确定哪种进入线程池的方法最适合您的需求:
这本电子书解释了这些并建议何时使用它们相比创建自己的线程。
FileStream
,应该使用 BeginRead
和 EndRead
。对于 HttpWebRequest
,应该使用 BeginGetResponse
和 EndGetResponse
。它们使用起来更加复杂,但是这是执行多线程 I/O 的正确方式。当进行任何显著、可变或未知处理时间的操作时,要注意.NET线程池可能会出现线程饥饿的情况。考虑使用.NET并行扩展,它们提供了许多逻辑抽象来处理线程操作。它们还包括一个新的调度器,应该比线程池更好。请参阅此处。
仅将线程池用于小任务的一个原因是,线程池线程数量有限。如果一个线程长时间被占用,则其他代码无法使用该线程。如果这种情况发生多次,则线程池可能会被用尽。
用尽线程池可能会产生微妙的影响-例如,一些.NET计时器使用线程池线程并且不会启动。
有关任务、线程和.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。