什么时候在C#中使用线程池?[关闭]


127

我一直在尝试学习C#中的多线程编程,对于何时使用线程池而不是创建自己的线程,我感到困惑。一本书建议仅将线程池用于小型任务(无论这意味着什么),但我似乎找不到任何真正的准则。在做出此编程决策时,需要考虑哪些因素?

Answers:


47

如果您有很多需要不断处理的逻辑任务,并且希望并行执行,请使用pool + scheduler。

如果您需要同时执行与IO相关的任务(例如从远程服务器下载内容或访问磁盘),但需要每隔几分钟执行一次,那么请创建自己的线程并在完成后将其杀死。

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

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

编辑:制作自己的线程的原因是由于上下文的更改,(即当线程需要与它们的内存一起换入和换出该进程时)。进行无用的上下文更改(例如,当您不使用线程时),就像他们可能说的那样,将它们闲置一圈,很容易会使程序的性能降低一半(例如,您有3个睡眠线程和2个活动线程)。因此,如果这些下载线程仅在等待,它们将消耗大量的CPU并冷却实际应用程序的缓存


2
好的,但是您能解释一下为什么这样做吗?例如,使用线程池从远程服务器下载或使用磁盘IO有什么弊端?

8
如果线程正在等待同步对象(事件,信号量,互斥量等),则该线程不会消耗CPU。
布兰农

7
正如Brannon所说,一个普遍的神话是创建多个线程确实会影响性能。实际上,未使用的线程消耗很少的资源。仅在需求量非常高的服务器中,上下文切换才开始成为问题(在这种情况下,请参阅I / O完成端口)。
FDCastel

12
空闲线程会影响性能吗?这取决于他们如何等待。如果写得很好并且在同步对象上等待,那么它们应该不消耗CPU资源。如果在周期性唤醒的循环中等待检查​​结果,则这是在浪费CPU。与往常一样,这取决于良好的编码。
比尔2009年

2
空闲的托管线程会占用其堆栈的内存。默认情况下,每个线程1 MiB。因此最好所有线程都在工作。
Vadym Stetsiak

48

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

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

对于小任务,您阅读的书意味着寿命短的任务。如果创建一个仅运行一秒钟的线程需要十秒钟,那么这就是您应该使用池的地方(忽略我的实际数字,这是重要的比率)。

否则,您将花费大量时间来创建和销毁线程,而不是简单地完成它们打算执行的工作。


28

这是.Net中线程池的一个不错的摘要:http : //blogs.msdn.com/pedram/archive/2007/08/05/dedicated-thread-or-a-threadpool-thread.aspx

这篇文章还指出了何时不应该使用线程池而应该启动自己的线程。


8
-1为链接。我确定这是一个很好的链接,但是我希望SO能够自给自足。
乔恩·戴维斯

26
@ stimpy77-那是错误的期望。SO永远不能自给自足,因为它既不是所有问题的最终权威,也不是(并且应该)在涉及该主题的每个SO答案中复制(并且应该)复制每个主题的所有深入信息。(而且我认为您没有足够的声誉来拒绝Jon Skeet的每个具有出站链接的答案,更不用说所有具有出站链接的SO用户的所有答案了:
Franci Penov

2
也许我太简洁了,也许我应该澄清一下。我不反对链接。我反对仅包含链接的答案。我不认为这是答案。现在,如果发布了答案的简短摘要以总结链接内容的应用方式,那将是可以接受的。此外,我到这里来寻找相同问题的答案,这个答案使我很恼火,因为这是我不得不单击的另一个链接,对于它可能会针对特定问题说什么有任何想法。无论如何,乔恩·斯基特(Jon Skeet)与这有何关系?我为什么要在乎呢?
乔恩·戴维斯

8
“您在发布此帖子两年后才来此帖子,而我现在在此处复制的任何内容现在可能已经过时了。” 链接也可能如此。发布链接时发布简洁但完整的摘要,您永远不知道链接是过时还是失效。
乔恩·戴维斯

2
我不同意这种观点:不是因为不可行而发布包含大量信息的帖子,也不是为此大声疾呼。我会说这是可能的链接变得比内容无法操作变得过时的/消除,虽然。因此,在场合允许的情况下,提供更多内容是不错的选择。我们都是(大多数)志愿者,所以要多谢您-感谢Franci :)
zanlok 2010年

14

我强烈建议您阅读这本免费的电子书: Joseph Albahari的C#线程

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

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

  • 任务并行库(.NET Framework 4.0)
  • ThreadPool.QueueUserWorkItem
  • 异步代表
  • 后台工作者

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


8

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

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

线程池旨在最大程度地跨CPU(或CPU内核)完成工作。这就是默认情况下线程池为每个处理器增加多个线程的原因。

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

Pax Diablo也很不错。加速线程不是免费的。这需要时间,并且它们会占用额外的内存以用于其堆栈空间。线程池将重新使用线程以分摊此费用。

注意:您询问有关使用线程池线程下载数据或执行磁盘I / O的问题。为此,您不应该使用线程池线程(由于上述原因)。而是使用异步I / O(也称为BeginXX和EndXX方法)。因为FileStream那是BeginReadand EndRead。因为HttpWebRequest那是BeginGetResponseand EndGetResponse。它们使用起来更加复杂,但是它们是执行多线程I / O的正确方法。


1
ThreadPool是一个聪明的自动化程序。“如果队列保持静止状态超过半秒,它将通过创建更多线程(每半秒一个)来响应,直至达到线程池的容量”(albahari.com/threading/#_Optimizing_the_Thread_Pool)。同样,通过ThreadPool使用BeginXXX-EndXXX进行几乎异步的操作。因此,通常使用ThreadPool下载数据并经常隐式使用它。
Artru

6

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


2
我们很难发现这一点!ASP.Net使用Threadpool出现了,因此我们不能按照我们的意愿使用它。
noocyte

3

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

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


2

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

在要创建多个线程的应用程序中,创建线程的开销非常大。使用线程池仅创建一次线程并重新使用它们,从而避免了线程创建的开销。

在我研究的应用程序中,从创建线程到将线程池用于短期线程的转变确实有助于应用程序的吞吐量。


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

2

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

关于任务与线程与.NET ThreadPool的许多文章未能真正为您提供决策性能所需的条件。但是当您比较它们时,线程胜出,尤其是线程池。它们在CPU之间分布最好,并且启动速度更快。

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

现在考虑一下现实:

我们大多数人不需要我们的应用程序具有确定性,并且我们大多数人没有线程的强硬背景,例如,线程通常是在开发操作系统时附带的。我上面写的内容不适合初学者。

因此,最重要的是讨论易于编程的东西。

如果创建自己的线程池,则需要做一些编写工作,因为您需要关注跟踪执行状态,如何模拟挂起和恢复以及如何取消执行-包括在整个应用程序范围内关掉。您可能还需要考虑是否要动态扩展池,以及池将具有什么容量限制。我可以在一个小时内编写一个这样的框架,但这是因为我已经做了很多次了。

编写执行单元的最简单方法也许就是使用Task。Task的优点在于您可以创建一个并在代码中直接将其启动(尽管可能需要谨慎)。您可以传递取消令牌来处理要取消的任务。同样,它使用promise方法链接事件,您可以让它返回特定类型的值。而且,有了async和await,存在更多选项,您的代码将更易于移植。

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

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

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

检测结果


1
仅供参考,忘记了,任务和.NET线程是在.NET中模拟的并发,并且管理是在.NET中执行的,而不是OS-后者在管理并发执行方面效率更高。我使用Tasks做很多事情,但是使用OS Thread来提高执行性能。MS声称Tasks和.NET Threads更好,但它们通常是为了平衡.NET应用程序之间的并发性。但是,服务器应用程序将让操作系统处理并发性能最佳。

希望看到您的自定义Threadpool的实现。很好写!
弗朗西斯

我不了解您的测试结果。das“单位冉”是什么​​意思?您将34个任务与512个线程进行比较?你能解释一下吗?
Elmue

Unit只是一种在Task,Thread或.NET ThreadPool工作线程中并发执行的方法,我的测试是比较启动/运行性能。每个测试有30秒的时间从头生成512个线程,512个任务,512个ThreadPool工作线程,或恢复一个512个启动线程的池,等待上下文执行。Tasks和ThreadPool工作线程的旋转速度很慢,因此30秒不足以将它们全部旋转。但是,如果首先将ThreadPool最小工作线程数设置为512,则Task和ThreadPool工作线程从头开始的旋转速度几乎与512个线程一样快。


1

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

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

在MSDN上查看页面:http : //msdn.microsoft.com/zh-cn/library/3dasc8as(VS.80).aspx


好的,我想这与我的另一个问题有关。您如何知道给定时间有多少个可用线程?

好吧,这很难说。您必须进行性能测试。在一个点之后添加更多线程将不会为您带来更多速度。找出机器上有多少个处理器,这将是一个很好的起点。然后从那里开始,如果处理速度没有提高,请不要添加更多线程。
lajos

1

如果可能,请始终使用线程池,并尽可能以最高的抽象水平进行工作。线程池为您隐藏了创建和销毁线程,这通常是一件好事!


1

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

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


1

不要忘记调查背景工作者。

我发现在很多情况下,它都能满足我的需求而无需繁重的工作。

干杯。


当它是一个可以继续运行的简单应用程序,并且您还有其他任务要做时,执行此代码非常容易。您没有提供链接:规格教程
zanlok 2010年

0

我通常在需要在另一个线程上执行某些操作时就使用Threadpool,而实际上并不关心它何时运行或结束。诸如记录日志或什至在后台下载文件之类的东西(尽管有更好的方法来执行异步样式)。我需要更多控制权时使用自己的线程。当我有多个需要在大于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()应该最好检查队列。Count>0。(如果使用Monitor.PulseAll或等待超时,则需要)
Craig Nicholson

0

我希望线程池能以尽可能少的延迟在内核之间分配工作,而不必与其他应用程序很好地配合。我发现.NET线程池的性能不尽如人意。我知道每个内核需要一个线程,所以我编写了自己的线程池替代类。该代码是这里另一个StackOverflow问题的答案。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.