何时在 C# 中使用线程池? [关闭]

Posted

技术标签:

【中文标题】何时在 C# 中使用线程池? [关闭]【英文标题】:When to use thread pool in C#? [closed] 【发布时间】:2010-09-13 19:17:38 【问题描述】:

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

线程池与创建自己的线程相比有哪些优缺点?每个都有哪些示例用例?

【问题讨论】:

【参考方案1】:

当您要处理的任务多于可用线程时,线程池非常有用。

您可以将所有任务添加到一个线程池中,并指定在某个时间可以运行的最大线程数。

查看 MSDN 上的 this 页面: http://msdn.microsoft.com/en-us/library/3dasc8as(VS.80).aspx

【讨论】:

好的,我想这与我的另一个问题有关。您如何知道在任何给定时间您有多少可用线程? 嗯,这很难说。您必须进行性能测试。在某个点之后添加更多线程不会给你更快的速度。找出机器上有多少个处理器,这将是一个很好的起点。然后从那里往上走,如果处理速度没有提高,就不要再增加线程了。【参考方案2】:

如果可以,请始终使用线程池,尽可能在最高抽象级别上工作。线程池为你隐藏创建和销毁线程,这通常是件好事!

【讨论】:

【参考方案3】:

如果您有大量需要持续处理的逻辑任务,并且您希望并行完成,请使用 pool+scheduler。

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

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

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

编辑:创建自己的线程的原因是因为上下文的变化,(当线程需要交换进出进程以及它们的内存时)。有无用的上下文更改,比如当你不使用你的线程时,只是让它们像人们所说的那样闲置,可以很容易地将你的程序性能降低一半(比如你有 3 个睡眠线程和 2 个活动线程)。因此,如果那些下载线程只是在等待,它们会消耗大量 CPU 并为您的实际应用程序冷却缓存

【讨论】:

好的,但是你能解释一下为什么你是这样处理的吗?比如使用线程池从远程服务器下载或者做磁盘IO有什么坏处? 如果线程正在等待同步对象(事件、信号量、互斥锁等),则该线程不会消耗 CPU。 正如 Brannon 所说,一个常见的误解是创建多个线程确实会影响性能。实际上,未使用的线程消耗的资源很少。上下文切换仅在需求量非常大的服务器中开始成为问题(在这种情况下,请参阅 I/O 完成端口以获取替代方案)。 空闲线程会影响性能吗?这取决于他们如何等待。如果写得好并等待同步对象,那么它们应该不会消耗 CPU 资源。如果在一个周期性地唤醒检查结果的循环中等待,那么它就是在浪费 CPU。与往常一样,它归结为良好的编码。 空闲托管线程会占用堆栈的内存。默认情况下,每个线程 1 MiB。所以最好让所有线程都工作。【参考方案4】:

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

如果您想限制正在运行的线程数或不想产生创建和销毁它们的开销,请使用线程池。

对于小任务,你读的书是指生命周期很短的任务。如果创建一个只运行一秒的线程需要 10 秒,那么这就是您应该使用池的地方(忽略我的实际数字,重要的是比率)。

否则,您会花费大量时间创建和销毁线程,而不是简单地做它们打算做的工作。

【讨论】:

【参考方案5】:

这是 .Net 中线程池的一个很好的总结:http://blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-a-threadpool-thread.aspx

这篇文章也有几点说明什么时候你不应该使用线程池,而是启动你自己的线程。

【讨论】:

-1 表示链接。我确信这是一个很好的链接,但我希望 SO 能够自给自足。 @stimpy77 - 那是错误的期望。 SO 永远不能自给自足,因为它既不是所有问题的最终权威,也不是每个主题的所有深入信息都可以(并且应该)在涉及该主题的每个 SO 答案中重复。 (而且我认为您甚至没有足够的声誉来否决具有出站链接的 Jon Skeet 的每个答案,更不用说所有具有出站链接的 SO 用户的所有答案了 :-)) 也许我说得太简洁了,也许我应该澄清一下。我不反对链接。我反对只包含链接的答案。我不认为这是一个答案。现在,如果已发布答案的简短摘要以总结链接内容的应用方式,那将是可以接受的。此外,我来这里是为了寻找同一个问题的答案,这个答案让我很恼火,因为这是我必须点击的另一个链接,以了解它可能会针对特定问题说什么。无论如何,Jon Skeet 与此有什么关系?我为什么要关心? “你在这篇文章发布两年后才来这里,我在这里复制的任何东西现在可能已经过时了。”所以可能是一个链接。发布链接时发布简洁但完整的摘要,您永远不知道链接是否过时或失效。 我不同意 stimpy:不是因为不可行而包含大量信息的帖子的想法,也不是为此而呼吁某人。不过,我会说链接变得无法操作的可能性更多,而不是内容被弃用/排除。因此,当场合允许时,更多的内容是好的。我们都是(大部分)志愿者,所以要感谢你所得到的 - 谢谢 Franci :)【参考方案6】:

线程池旨在减少线程之间的上下文切换。考虑一个运行多个组件的进程。这些组件中的每一个都可以创建工作线程。进程中的线程越多,在上下文切换上浪费的时间就越多。

现在,如果这些组件中的每一个都将项目排队到线程池中,那么上下文切换开销就会大大减少。

线程池旨在最大限度地利用 CPU(或 CPU 内核)完成的工作。这就是为什么默认情况下,线程池会为每个处理器启动多个线程。

在某些情况下您不想使用线程池。如果您正在等待 I/O,或等待事件等,那么您会占用该线程池线程,并且其他任何人都无法使用它。同样的想法也适用于长时间运行的任务,尽管构成长时间运行的任务是主观的。

Pax Diablo 也提出了一个很好的观点。旋转线程不是免费的。这需要时间,并且它们会为堆栈空间消耗额外的内存。线程池将重用线程来分摊此成本。

注意:您询问了有关使用线程池线程下载数据或执行磁盘 I/O 的问题。您不应该为此使用线程池线程(出于我上面概述的原因)。而是使用异步 I/O(又名 BeginXX 和 EndXX 方法)。对于FileStream,这将是BeginReadEndRead。对于HttpWebRequest,这将是BeginGetResponseEndGetResponse。它们使用起来更复杂,但它们是执行多线程 I/O 的正确方法。

【讨论】:

ThreadPool 是一个聪明的自动化。 “如果它的队列保持静止超过半秒,它会通过创建更多线程来响应——每半秒一个——直到线程池的容量”(albahari.com/threading/#_Optimizing_the_Thread_Pool)。 BeginXXX-EndXXX 的几乎异步操作也是通过 ThreadPool 使用的。所以使用ThreadPool下载数据是很正常的,经常隐式使用。【参考方案7】:

大多数时候您可以使用池,因为您可以避免创建线程的昂贵过程。

但是在某些情况下您可能想要创建一个线程。例如,如果您不是唯一使用线程池的人,并且您创建的线程是长期存在的(以避免消耗共享资源),或者例如,如果您想控制线程的堆栈大小。

【讨论】:

【参考方案8】:

仅将线程池用于小型任务的一个原因是线程池线程的数量有限。如果一个被使用了很长时间,那么它会阻止该线程被其他代码使用。如果这种情况发生很多次,那么线程池可能会被用完。

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

【讨论】:

【参考方案9】:

如果您的后台任务会存在很长时间,例如应用程序的整个生命周期,那么创建自己的线程是合理的事情。如果您有需要在线程中完成的短作业,请使用线程池。

在创建许多线程的应用程序中,创建线程的开销会变得很大。使用线程池创建线程一次并重用它们,从而避免线程创建开销。

在我从事的一个应用程序中,从创建线程改为使用线程池来处理短期线程确实有助于提高应用程序的吞吐量。

【讨论】:

请澄清您的意思是“线程池”还是“线程池”。这些是非常不同的东西(至少在 MS CLR 中)。【参考方案10】:

注意 .NET 线程池中的操作可能会阻塞其处理的任何重要、可变或未知部分,因为它很容易出现线程饥饿。考虑使用 .NET 并行扩展,它为线程操作提供了大量的逻辑抽象。它们还包括一个新的调度程序,这应该是对 ThreadPool 的改进。见here

【讨论】:

我们发现这一点很艰难! ASP.Net 使用了 Threadpool,所以我们不能像我们希望的那样使用它。【参考方案11】:

每当我需要在另一个线程上做某事并且并不真正关心它何时运行或结束时,我通常会使用线程池。诸如日志记录甚至后台下载文件之类的东西(尽管有更好的方法可以实现异步风格)。当我需要更多控制时,我会使用自己的线程。我还发现,当我有多个需要在 >1 线程中处理的命令时,使用 Threadsafe 队列(破解你自己的)来存储“命令对象”很好。因此,您可能会拆分一个 Xml 文件并将每个元素放入一个队列中,然后让多个线程对这些元素进行一些处理。我在 uni (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();
        
    

【讨论】:

这种方法的一些问题: - 对 DequeueSafe() 的调用将等到一个项目进入 EnqueuedSafe()。考虑使用指定超时的 Monitor.Wait() 重载之一。 - 锁定它不是根据最佳实践,而是创建一个只读对象字段。 - 尽管 Monitor.Pulse() 是轻量级的,但在队列仅包含 1 个项目时调用它会更有效。 - DeEnqueueUnblock() 最好检查 queue.Count > 0。(如果使用 Monitor.PulseAll 或等待超时,则需要)【参考方案12】:

别忘了调查后台工作人员。

我发现在很多情况下,它给了我我想要的东西,而不需要繁重的工作。

干杯。

【讨论】:

当它是一个保持运行的简单应用程序并且您还有其他任务要做时,很容易执行此代码。您没有提供链接:specification 和 tutorial【参考方案13】:

我想要一个线程池以尽可能少的延迟在内核之间分配工作,并且不必与其他应用程序很好地配合。我发现 .NET 线程池的性能并没有达到应有的水平。我知道我希望每个核心有一个线程,所以我编写了自己的线程池替代类。该代码是作为另一个 *** 问题 over here 的答案提供的。

至于最初的问题,线程池对于将重复计算分解成可以并行执行的部分很有用(假设它们可以并行执行而不改变结果)。手动线程管理对于 UI 和 IO 等任务很有用。

【讨论】:

【参考方案14】:

我强烈推荐阅读这本免费的电子书: Threading in C# by Joseph Albahari

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

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

任务并行库 (.NET Framework 4.0) ThreadPool.QueueUserWorkItem 异步委托 后台工作人员

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

【讨论】:

【参考方案15】:

为了获得并发执行单元的最高性能,请编写自己的线程池,其中一个线程对象池在启动时创建并进入阻塞(以前暂停),等待上下文运行(具有标准的对象由您的代码实现的接口)。

关于任务、线程和 .NET 线程池的大量文章未能真正为您提供做出性能决定所需的内容。但是当你比较它们时,线程会胜出,尤其是线程池。它们在 CPU 中分布得最好,而且启动速度更快。

要讨论的是Windows(包括Windows 10)的主要执行单元是线程,OS上下文切换开销通常可以忽略不计。简而言之,我无法找到其中许多文章的令人信服的证据,无论文章声称通过节省上下文切换或更好的 CPU 使用率来提高性能。

现在有点现实主义:

我们大多数人不需要我们的应用程序是确定性的,而且我们大多数人都没有线程的硬敲背景,例如,开发操作系统时通常会附带线程。我上面写的不是给初学者的。

所以最重要的可能是讨论什么是易于编程的。

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

也许编写执行单元的最简单方法是使用任务。任务的美妙之处在于您可以创建一个并在代码中内联启动它(尽管可能需要谨慎)。当你想取消任务时,你可以传递一个取消令牌来处理。此外,它使用 Promise 方法链接事件,您可以让它返回特定类型的值。此外,使用 async 和 await,存在更多选项,并且您的代码将更具可移植性。

从本质上讲,了解 Tasks、Threads 和 .NET ThreadPool 的优缺点很重要。如果我需要高性能,我将使用线程,并且我更喜欢使用自己的池。

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

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

Test Results

【讨论】:

仅供参考,忘了提到任务和 .NET 线程是 .NET 中的模拟并发,并且在 .NET 中执行管理而不是操作系统 - 后者在管理并发执行方面效率更高。我将任务用于很多事情,但我使用 OS 线程来提高执行性能。 MS 声称任务和 .NET 线程更好,但它们通常用于平衡 .NET 应用程序之间的并发性。然而,服务器应用程序最好让操作系统处理并发。 希望看到您的自定义线程池的实现。写得真好! 我不明白你的测试结果。 “单位跑”是什么意思?您将 34 任务与 512 线程进行比较?你能解释一下吗? Unit 只是一种在 Task、Thread 或 .NET ThreadPool 工作线程中并发执行的方法,我的测试比较了启动/运行性能。每个测试有 30 秒的时间从头开始生成 512 个线程、512 个任务、512 个线程池工作线程,或者恢复一个由 512 个启动线程组成的池,等待上下文执行。 Tasks 和 ThreadPool 工作线程的启动速度很慢,因此 30 秒不足以将它们全部启动。但是,如果首先将 ThreadPool 最小工作线程计数设置为 512,则 Tasks 和 ThreadPool 工作线程的启动速度几乎与从头开始的 512 个线程一样快。 @Francis github.com/grabe/NativeWindowsThreadPool

以上是关于何时在 C# 中使用线程池? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

如何优雅的关闭线程池?

C#多线程--线程池(ThreadPool)

c# 怎么等待线程池中所有线程都运行结束在运行主线程

C#多线程之线程池篇3

C# 线程知识--使用Task执行异步操作

C#多线程编程:线程池ThreadPool