【问题标题】:ThreadPool.QueueUserWorkItem vs Task.Factory.StartNewThreadPool.QueueUserWorkItem 与 Task.Factory.StartNew
【发布时间】:2012-03-01 07:35:30
【问题描述】:

下面有什么区别

ThreadPool.QueueUserWorkItem

Task.Factory.StartNew

如果某个长时间运行的任务调用上述代码500次,是否意味着所有线程池线程都会被占用?

或者 TPL(第二个选项)是否足够聪明,只占用少于或等于处理器数量的线程?

【问题讨论】:

    标签: c# multithreading c#-4.0 task-parallel-library threadpool


    【解决方案1】:

    如果您要使用 TPL 启动一个长时间运行的任务,您应该指定 TaskCreationOptions.LongRunning,这意味着它不会将它安排在线程池中。 (编辑:正如 cmets 中所述,这 特定于调度程序的决定,并不是一个硬性和快速的保证,但我希望任何明智的生产调度程序都会避免调度长时间运行的任务在线程池上。)

    您绝对不应该自己在线程池上安排大量长时间运行的任务。我相信现在线程池的默认大小相当大(因为它经常以这种方式被滥用)但基本上不应该这样使用。

    线程池的重点是避免 任务因创建新线程而受到很大影响,与它们实际运行的时间相比。如果任务将运行很长时间,那么创建新线程的影响无论如何都会相对较小 - 而且您不希望最终可能耗尽线程池线程。 (现在不太可能,但我确实在早期版本的 .NET 上体验过。)

    就个人而言,如果我可以选择,我肯定会使用 TPL,因为 Task API 非常好 - 但记得告诉 TPL 你希望任务运行很久了。

    编辑:如 cmets 中所述,另请参阅 PFX 团队在 choosing between the TPL and the thread pool 上的博客文章:

    最后,我将重申 CLR 团队的 ThreadPool 开发人员已经说过的内容:

    Task is now the preferred way to queue work to the thread pool.
    

    编辑:同样来自 cmets,不要忘记 TPL 允许您使用 custom schedulers,如果您真的想...

    【讨论】:

    • 我对@9​​87654329@ 将始终避免使用线程池的硬性规则持谨慎态度。它似乎更像是一个指令而不是实施保证。我是不是在这个基础上?
    • @Marc:嗯,这取决于调度程序 - 但如果在线程池上明确调度长时间运行的任务,IMO 将是一个非常疯狂的调度程序。
    • @Brad:谢谢,将添加一个链接到我的答案。
    • 我还要补充一点,TPL 允许您指定自己的调度程序,包括允许您控制自己的并发的自定义调度程序:msdn.microsoft.com/en-us/library/ee789351.aspx
    【解决方案2】:

    不,通过使用Task.Factory.StartNew 方法(或更现代的Task.Run 方法),ThreadPool 线程的使用方式并没有增加额外的聪明之处。调用Task.Factory.StartNew 500 次(长时间运行的任务)肯定会使ThreadPool 饱和,并使其长时间保持饱和状态。这不是一个好的情况,因为饱和的ThreadPool 会对在这 500 个启动任务期间也可能处于活动状态的任何其他独立回调、计时器事件、异步继续等产生负面影响。

    Task.Factory.StartNew 方法计划在TaskScheduler.Current 上执行提供的Action,默认情况下是TaskScheduler.Default,即internal ThreadPoolTaskScheduler 类。这是ThreadPoolTaskScheduler.QueueTask方法的the implementation

    protected internal override void QueueTask(Task task)
    {
        if ((task.Options & TaskCreationOptions.LongRunning) != 0)
        {
            // Run LongRunning tasks on their own dedicated thread.
            Thread thread = new Thread(s_longRunningThreadWork);
            thread.IsBackground = true; // Keep this thread from blocking process shutdown
            thread.Start(task);
        }
        else
        {
            // Normal handling for non-LongRunning tasks.
            bool forceToGlobalQueue = ((task.Options & TaskCreationOptions.PreferFairness) != 0);
            ThreadPool.UnsafeQueueCustomWorkItem(task, forceToGlobalQueue);
        }
    }
    

    正如您所见,任务的执行无论如何都安排在ThreadPool 上。 ThreadPool.UnsafeQueueCustomWorkItemThreadPool 类的 internal 方法,并且有一些未公开的细微差别 (bool forceGlobal)。但是其中没有任何内容可以改变ThreadPool 在饱和时的行为¹。目前,这种行为也不是特别复杂。线程注入算法只是每 500 毫秒在池中注入一个新线程,直到饱和事件结束。

    ¹ 当工作需求超过线程的当前可用性时,ThreadPool 被称为饱和,并且已达到不再按需创建新线程的阈值SetMinThreads。子>

    【讨论】:

    • 我不明白..您发布了正确的 sn-p 至极,表明如果您通过 LongRunning 则没有使用线程池线程。但你说你回答的对立面?
    • @ManuelAmstutz OP 没有在问题中提到TaskCreationOptions.LongRunning 配置,所以我假设他们在以最简单的形式使用Task.Factory.StartNew 时询问它:var task = Task.Factory.StartNew(() => /* ... */);。这是此方法最常见的使用模式。或者至少这是我在本网站上接触过数百个与 TPL 相关的问题后的印象。
    猜你喜欢
    • 2011-09-05
    • 2011-03-04
    • 2014-07-29
    • 1970-01-01
    • 2014-01-08
    • 2016-04-22
    • 2011-06-27
    • 1970-01-01
    相关资源
    最近更新 更多