Task.Run()和Task.Factory.StartNew()有什么区别


190

我有方法:

private static void Method()
{
    Console.WriteLine("Method() started");

    for (var i = 0; i < 20; i++)
    {
        Console.WriteLine("Method() Counter = " + i);
        Thread.Sleep(500);
    }

    Console.WriteLine("Method() finished");
}

我想在新任务中启动此方法。我可以像这样开始新任务

var task = Task.Factory.StartNew(new Action(Method));

或这个

var task = Task.Run(new Action(Method));

但是Task.Run()和之间有什么区别Task.Factory.StartNew()。创建Task实例后,他们两个都立即使用ThreadPool并启动Method()。什么时候应该使用第一个变量,什么时候应该使用第二个变量?


6
实际上,StartNew不必使用ThreadPool,请参阅我在答案中链接到的博客。问题是StartNew默认情况下使用TaskScheduler.Current,它可能是线程池,也可能是UI线程。
Scott Chamberlain

Answers:


195

Task.Run在更高版本的.NET框架(在.NET 4.5中)中引入了第二种方法。

但是,第一种方法可以Task.Factory.StartNew为您提供有关要创建的线程定义很多有用的东西的机会,而Task.Run没有提供此功能。

例如,假设您要创建一个长时间运行的任务线程。如果线程池中的一个线程将用于此任务,则可以认为这是对线程池的滥用。

为了避免这种情况,您可以做的一件事就是在单独的线程中运行任务。一个新创建的线程,专用于此任务,一旦完成任务,该线程将被销毁。不能使用来实现此目的Task.Run,而可以使用实现此目的Task.Factory.StartNew,如下所示:

Task.Factory.StartNew(..., TaskCreationOptions.LongRunning);

由于这是说在这里

因此,在.NET Framework 4.5开发人员预览版中,我们引入了新的Task.Run方法。 这绝不会淘汰 Task.Factory.StartNew, 而应该只是将其视为使用 Task.Factory.StartNew 的快速方法而无需指定一堆参数。这是捷径。 实际上,Task.Run实际上是根据用于Task.Factory.StartNew的相同逻辑来实现的,只是传递了一些默认参数。将Action传递给Task.Run时:

Task.Run(someAction);

完全等同于:

Task.Factory.StartNew(someAction, 
    CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

4
我有一段代码,其中statemente that’s exactly equivalent to不成立。
Emaborsa

7
@Emaborsa,如果您可以发布这段代码并阐述您的观点,我将不胜感激。提前致谢 !
Christos'Sep

4
@Emaborsa您可以创建要点gist.github.com并共享它。但是,除了分享这一要点外,请指定您如何获得该词组tha's exactly equivalent to不成立的结果。提前致谢。最好在代码中加上注释来解释。谢谢:)
Christos

8
还值得一提的是Task.Run默认情况下会解开嵌套任务。我建议阅读这篇文章的主要区别:blogs.msdn.microsoft.com/pfxteam/2011/10/24/...
帕维尔马夹

1
@ The0bserver不,是TaskScheduler.Default。请在此处查看referencesource.microsoft.com/#mscorlib/system/threading/Tasks/…
Christos

46

人们已经提到

Task.Run(A);

相当于

Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);

但是没有人提到

Task.Factory.StartNew(A);

等效于:

Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Current);

如您所见,Task.Run和的两个参数不同Task.Factory.StartNew

  1. TaskCreationOptions- Task.Run使用TaskCreationOptions.DenyChildAttach表示子任务不能附加到父任务,请考虑以下事项:

    var parentTask = Task.Run(() =>
    {
        var childTask = new Task(() =>
        {
            Thread.Sleep(10000);
            Console.WriteLine("Child task finished.");
        }, TaskCreationOptions.AttachedToParent);
        childTask.Start();
    
        Console.WriteLine("Parent task finished.");
    });
    
    parentTask.Wait();
    Console.WriteLine("Main thread finished.");

    当我们调用时parentTask.Wait()childTask即使我们指定TaskCreationOptions.AttachedToParent了它,也不会等待,这是因为TaskCreationOptions.DenyChildAttach禁止孩子附加到它。如果您使用Task.Factory.StartNew代替来运行相同的代码Task.RunparentTask.Wait()则将等待使用,childTask因为Task.Factory.StartNewTaskCreationOptions.None

  2. TaskScheduler- Task.Run使用TaskScheduler.Default表示默认任务计划程序(在线程池上运行任务的计划程序)将始终用于运行任务。Task.Factory.StartNew另一方面使用TaskScheduler.Current意味着当前线程的调度程序,它可能是TaskScheduler.Default但并非总是如此。实际上,在开发WinformsWPF应用程序时,需要从当前线程更新UI,此操作使人们可以使用TaskScheduler.FromCurrentSynchronizationContext()任务计划程序,如果您无意在使用该TaskScheduler.FromCurrentSynchronizationContext()计划程序的任务内部创建了另一个长期运行的任务,则UI将会冻结。可以在这里找到更详细的解释

因此,通常,如果您不使用嵌套的子级任务,并且始终希望在线程池上执行任务,则最好使用Task.Run,除非您有一些更复杂的方案。


1
这是一个绝妙的秘诀,应该是被接受的答案
Ali Bayat

30

请参阅描述差异的博客文章。基本上在做:

Task.Run(A)

与执行相同:

Task.Factory.StartNew(A, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);   

28

Task.Run得到了在新的.NET framework版本推出,它是推荐

从.NET Framework 4.5开始,建议使用Task.Run方法来启动计算绑定任务。仅当您需要对长时间运行的,计算绑定的任务进行细粒度控制时,才使用StartNew方法。

Task.Factory.StartNew有更多的选择,Task.Run是一个简写:

Run方法提供了一组重载,这些重载使使用默认值轻松启动任务。它是StartNew重载的轻型替代方案。

简而言之,我的意思是技术捷径

public static Task Run(Action action)
{
    return Task.InternalStartNew(null, action, null, default(CancellationToken), TaskScheduler.Default,
        TaskCreationOptions.DenyChildAttach, InternalTaskOptions.None, ref stackMark);
}

21

根据Stephen Cleary的这篇文章,Task.Factory.StartNew()很危险:

我在博客和使用Task.Factory.StartNew的SO问题中看到很多代码,这些代码在后台线程上加速工作。Stephen Toub有一篇出色的博客文章,解释了为什么Task.Run比Task.Factory.StartNew更好,但是我认为很多人都没有读过(或者不理解)。因此,我采用了相同的论点,添加了一些更强有力的语言,我们将看看情况如何。:) StartNew确实提供了比Task.Run更多的选项,但是我们将看到,它非常危险。在异步代码中,您应该首选Task.Run而不是Task.Factory.StartNew。

实际原因如下:

  1. 不了解异步委托。实际上,这与第1点相同,因为您希望使用StartNew。问题在于,当您将异步委托传递给StartNew时,自然会假定返回的任务代表该委托。但是,由于StartNew无法理解异步委托,因此该任务实际代表的只是该委托的开始。这是在异步代码中使用StartNew时,编码人员遇到的第一个陷阱。
  2. 令人困惑的默认调度程序。好的,提问时间:在下面的代码中,方法“ A”在哪个线程上运行?
Task.Factory.StartNew(A);

private static void A() { }

好吧,您知道这是一个棘手的问题,是吗?如果您回答“线程池线程”,很抱歉,但这是不正确的。“ A”将在当前正在执行的TaskScheduler上运行!

因此,这意味着它可能在操作完成后可能在UI线程上运行,并且由于继续执行而将其编组回UI线程,正如Stephen Cleary在其帖子中进行了更全面的解释。

就我而言,我试图在为视图加载数据网格时在后台运行任务,同时还显示繁忙的动画。使用时没有显示忙碌的动画,Task.Factory.StartNew()但切换到时,动画正确显示Task.Run()

有关详细信息,请参阅https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html


1

除了相似之处(即Task.Run()是Task.Factory.StartNew()的简写)外,在同步和异步委托的情况下,它们的行为之间存在细微的差别。

假设有以下两种方法:

public async Task<int> GetIntAsync()
{
    return Task.FromResult(1);
}

public int GetInt()
{
    return 1;
}

现在考虑以下代码。

var sync1 = Task.Run(() => GetInt());
var sync2 = Task.Factory.StartNew(() => GetInt());

这里sync1和sync2的类型均为Task <int>

但是,异步方法会有所不同。

var async1 = Task.Run(() => GetIntAsync());
var async2 = Task.Factory.StartNew(() => GetIntAsync());

在这种情况下,async1的类型为Task <int>,但是async2的类型为Task <Task <int>>


是的,因为Task.Run内置了Unwrap方法的展开功能。是一篇博客文章,解释了此决定背后的原因。
Theodor Zoulias

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.