我一直在尝试学习 C # 中的多线程编程,但我对什么时候使用线程池比创建自己的线程更好感到困惑。有一本书建议只对小任务使用线程池(不管这意味着什么) ,但我似乎找不到任何真正的指导方针。
与创建自己的线程相比,线程池有哪些优点和缺点?每个用例都有哪些示例用例?
当需要处理的任务比可用线程多时,线程池非常有用。
您可以将所有任务添加到线程池中,并指定在某个时间可以运行的最大线程数。
查看 MSDN 上的 这个页面: Http://msdn.microsoft.com/en-us/library/3dasc8as(vs.80).aspx
如果可能的话,总是使用线程池,尽可能在最高的抽象级别上工作。线程池隐藏为您创建和销毁线程,这通常是一件好事!
如果您有许多需要不断处理的逻辑任务,并希望这些任务并行完成,请使用 pool + 调度程序。
如果您需要并发执行与 IO 相关的任务,比如从远程服务器下载东西或磁盘访问,但是需要每隔几分钟执行一次,那么可以创建自己的线程,并在完成后将其终止。
编辑: 关于一些考虑,我使用线程池来访问数据库、物理/模拟、 AI (游戏) ,以及运行在处理大量用户定义任务的虚拟机上的脚本任务。
通常一个池由每个处理器2个线程组成(现在可能是4个) ,但是如果您知道需要多少个线程,您可以设置所需的线程数量。
编辑: 创建自己的线程的原因是上下文更改(即线程需要在进程内和进程外以及它们的内存中进行交换)。使用无用的上下文更改,比如说当您不使用线程时,只是让它们闲置,可以轻松地将程序的性能降低一半(比如说您有3个睡眠线程和2个活动线程)。因此,如果那些下载线程只是在等待,那么它们将消耗大量的 CPU,并为您的实际应用程序降低缓存的温度
我建议您在 C # 中使用线程池,其原因与其他语言相同。
如果希望限制正在运行的线程的数量,或者不想增加创建和销毁线程的开销,请使用线程池。
对于小任务来说,你读的书指的是生命短暂的任务。如果创建一个只运行一秒钟的线程需要10秒钟,那么就应该在这个地方使用池(忽略我的实际数据,重要的是比率)。
否则,您将花费大量时间创建和销毁线程,而不是简单地完成它们应该完成的工作。
下面是对.Net: http://blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-a-threadpool-thread.aspx中的线程池的一个很好的总结
这篇文章还提到了一些你不应该使用线程池而是启动你自己的线程的时候。
线程池旨在减少线程之间的上下文切换。考虑一个运行了多个组件的流程。每个组件都可以创建工作线程。进程中的线程越多,在上下文切换上浪费的时间就越多。
现在,如果每个组件都将项排队到线程池,那么上下文切换开销就会小得多。
线程池的设计目的是最大化在 CPU (或 CPU 核心)上正在完成的工作。这就是为什么在默认情况下,线程池为每个处理器旋转多个线程的原因。
在某些情况下,您不希望使用线程池。如果你正在等待 I/O,或者等待一个事件,等等,然后你绑定了线程池线程,它不能被任何人使用。同样的想法也适用于长时间运行的任务,尽管构成长时间运行任务的因素是主观的。
暗黑和平也说得很有道理。旋转线程并不是免费的。这需要时间,而且它们会为堆栈空间消耗额外的内存。线程池将重新使用线程来分摊这一成本。
注意: 您询问使用线程池线程下载数据或执行磁盘 I/O。您不应该为此使用线程池线程(出于上面概述的原因)。而是使用异步 I/O (也就是 BeginXX 和 EndXX 方法)。对于一个 FileStream将是 BeginRead和 EndRead。对于一个 HttpWebRequest,那将是 BeginGetResponse和 EndGetResponse。它们的使用更加复杂,但是它们是执行多线程 I/O 的正确方法。
FileStream
BeginRead
EndRead
HttpWebRequest
BeginGetResponse
EndGetResponse
大多数情况下,您可以使用池来避免创建线程的昂贵过程。
但是在某些情况下,您可能希望创建一个线程。例如,如果您不是唯一使用线程池的用户,并且您创建的线程是长寿命的(以避免消耗共享资源) ,或者如果您想控制线程的堆栈大小。
仅将线程池用于小型任务的一个原因是线程池线程的数量有限。如果一个线程被长时间使用,那么它将阻止该线程被其他代码使用。如果这种情况发生很多次,那么线程池就会用完。
使用线程池可能会产生一些微妙的效果。NET 计时器使用线程池线程,不会触发,例如。
如果您有一个后台任务可以存在很长时间,比如应用程序的整个生命周期,那么创建您自己的线程是一件合理的事情。如果您有需要在线程中完成的短作业,那么使用线程池。
在创建许多线程的应用程序中,创建线程的开销变得很大。使用线程池创建线程一次并重用它们,从而避免了线程创建开销。
在我工作过的一个应用程序中,从创建线程改为使用线程池处理短期线程确实有助于提高应用程序的吞吐量。
小心。NET 线程池,用于可能阻塞其处理的任何重要、可变或未知部分的操作,因为它容易出现线程饥饿。考虑使用。NET 并行扩展,它通过线程操作提供了大量的逻辑抽象。它们还包括一个新的调度程序,这应该是对 ThreadPool 的改进。参见 给你
当我需要在另一个线程上执行某些操作时,我通常使用 Threadpool,并不真正关心它何时运行或结束。比如日志记录,或者甚至是后台下载文件(尽管有更好的异步方式)。当我需要更多控制时,我会使用我自己的线程。我还发现,当我有多个命令需要在 > 1个线程中处理时,使用 Threadsafe 队列(黑进自己的队列)来存储“命令对象”非常好。因此,您可以分解一个 XML 文件,将每个元素放入一个队列中,然后让多个线程对这些元素进行一些处理。我在大学的时候就写过这样一个队列(VB.net!)我转换成了 C # 。我在下面没有什么特别的原因(这段代码可能包含一些错误)。
using System.Collections.Generic; using System.Threading; namespace ThreadSafeQueue { public class ThreadSafeQueue<T> { private Queue<T> _queue; public ThreadSafeQueue() { _queue = new Queue<T>(); } public void EnqueueSafe(T item) { lock ( this ) { _queue.Enqueue(item); if ( _queue.Count >= 1 ) Monitor.Pulse(this); } } public T DequeueSafe() { lock ( this ) { while ( _queue.Count <= 0 ) Monitor.Wait(this); return this.DeEnqueueUnblock(); } } private T DeEnqueueUnblock() { return _queue.Dequeue(); } } }
别忘了调查背景工作者。
我发现在很多情况下,它给了我我想要的东西,而不是繁重的工作。
干杯。
我想要一个线程池,以尽可能少的延迟跨核心分发工作,而这并不适用于其他应用程序。我发现。NET 线程池的性能并没有想象中的那么好。我知道每个核需要一个线程,所以我编写了自己的线程池替代类。该代码是作为对另一个 StackOverflow 问题 这边的回答而提供的。
对于最初的问题,线程池有助于将重复计算分解成可以并行执行的部分(假设它们可以并行执行而不改变结果)。手动线程管理对于 UI 和 IO 这样的任务非常有用。
我强烈推荐阅读这本免费的电子书: C # 中的线程作者: Joseph Albahari
至少阅读“入门”部分。该电子书提供了一个伟大的介绍,并包括大量先进的线程信息以及。
知道是否使用线程池仅仅是个开始。接下来,您需要确定进入线程池的哪种方法最适合您的需要:
这本电子书解释了所有这些,并建议何时使用它们还是创建自己的线程。
为了获得具有并发执行单元的最高性能,编写您自己的线程池,其中在启动时创建一个 Thread 对象池,然后进入阻塞(以前是暂停)状态,等待上下文运行(一个具有由您的代码实现的标准接口的对象)。
有很多关于任务、线程和。NET ThreadPool 无法真正为您提供决策性能所需的内容。但是当您比较它们时,Threads 胜出,特别是一个 Threads 池。它们最好地分布在 CPU 之间,并且启动速度更快。
需要讨论的是 Windows (包括 Windows 10)的主要执行单元是一个线程,而操作系统上下文切换开销通常可以忽略不计。简而言之,我无法找到这些文章中的许多令人信服的证据,无论这篇文章声称通过节省上下文切换或更好的 CPU 使用来获得更高的性能。
现在来点现实主义的:
我们中的大多数人不需要我们的应用程序是确定性的,我们中的大多数人也没有严格的线程背景,例如,这通常伴随着开发操作系统。我上面写的内容不适合初学者。
所以最重要的是讨论什么是容易编程的。
如果您创建自己的线程池,您将需要进行一些编写工作,因为您需要关注跟踪执行状态、如何模拟挂起和恢复以及如何取消执行——包括在应用程序范围的关闭中。您可能还必须考虑是否要动态增长池以及池的容量限制。我可以在一个小时内编写这样一个框架,但这是因为我已经做过很多次了。
也许编写执行单元的最简单方法是使用 Task。Task 的美妙之处在于,您可以创建一个 Task,并在代码中以内联方式启动它(尽管可能需要谨慎)。当您要取消任务时,可以传递要处理的取消令牌。此外,它使用承诺方法来链接事件,您可以让它返回特定类型的值。此外,使用异步和 wait 时,存在更多的选项,并且代码将更具可移植性。
本质上,了解任务、线程和。NET 线程池。如果需要高性能,我将使用线程,而且我更喜欢使用自己的池。
比较的一种简单方法是启动512个线程、512个任务和512个 ThreadPool 线程。您会发现在开始使用 Threads 时有一个延迟(因此,为什么要写一个线程池) ,但是所有512个线程将在几秒钟内运行,而 Tasks 和。NETThreadPool 线程需要几分钟才能全部启动。
下面是这样一个测试的结果(i5四核,16GB RAM) ,每30秒运行一次。执行的代码在 SSD 驱动器上执行简单的文件 I/O。
测试结果