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

我有方法:

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()之间有什么区别吗?它们都在使用ThreadPool,并在创建Task实例后立即启动Method()。什么时候用第一种,什么时候用第二种?

156146 次浏览

Task.Run是在更新的。net框架版本中引入的,它是推荐

从. net Framework 4.5开始,任务。运行方法是 推荐的启动计算绑定任务的方法。使用StartNew 方法仅在需要对长时间运行的 计算任务。< / p >

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

Run方法提供了一组重载,使它易于启动 使用默认值的任务。它是一种轻量级的替代 StartNew过载。< / p >

这里我指的是技术上的快捷方式:

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

第二个方法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.Factory.StartNew, 而应该简单地认为这是一种快速使用的方法 Task.Factory.StartNew 而不需要指定一堆 参数。这是一条近路。事实上,任务。Run实际上是 以Task.Factory.StartNew相同的逻辑实现, 只是传入一些默认参数。当您将一个Action传递给 的任务。运行:< / p >
Task.Run(someAction);

这完全等价于:

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

请参见这篇博文描述区别。主要做:

Task.Run(A)

就等于做了:

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

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

我在博客和SO问题中看到很多代码使用Task.Factory.StartNew在后台线程上启动工作。Stephen Toub有一篇很棒的博客文章解释了为什么Task。Run比Task.Factory更好。StartNew,但我认为很多人只是没有读过它(或不理解它)。所以,我采取了同样的论点,增加了一些更有力的语言,我们会看到它是如何发展的。:) StartNew提供了比Task更多的选项。快跑,但这很危险,我们会看到的。你应该选择Task。在async代码中运行Task.Factory.StartNew .

以下是真正的原因:

  1. 不理解异步委托。这实际上和 第一点是你想要使用StartNew的原因。这个问题 当你传递一个异步委托给StartNew,这是很自然的吗 假设返回的任务代表该委托。然而,由于 StartNew不理解async委托,任务到底是什么 代表只是委托的开始。这是其中之一 程序员在async中使用StartNew时遇到的第一个陷阱 代码。李< / >
  2. 混淆默认调度器。好了,疑难题目时间到了 下面的代码,方法“A”运行在哪个线程上?李< / >
Task.Factory.StartNew(A);


private static void A() { }
好吧,你知道这是一个陷阱问题,嗯?如果你回答“一根线” pool thread ",很抱歉,这是错误的。A会继续 不管TaskScheduler当前正在执行什么!< / p >

所以这意味着如果一个操作完成,它可能会运行在UI线程上,并且由于一个延续,它会封送回UI线程,Stephen Cleary在他的帖子中解释得更详细。

在我的例子中,我试图在加载一个视图的数据网格时在后台运行任务,同时还显示一个繁忙的动画。使用Task.Factory.StartNew()时,忙碌的动画没有显示,但当我切换到Task.Run()时,动画显示正常。

详情请参见https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html

人们已经提到过

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.RunTask.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.Run运行相同的代码,parentTask.Wait()将等待childTask,因为Task.Factory.StartNew使用了TaskCreationOptions.None

  1. TaskScheduler - Task.Run使用TaskScheduler.Default,这意味着默认的任务调度器(在线程池上运行任务的调度器)将始终用于运行任务。另一方面,Task.Factory.StartNew使用TaskScheduler.Current,这意味着当前线程的调度器,它可能是TaskScheduler.Default,但并不总是如此。事实上,在开发WinformsWPF应用程序时,需要从当前线程更新UI,为此人们使用TaskScheduler.FromCurrentSynchronizationContext()任务调程器,如果你无意中在使用TaskScheduler.FromCurrentSynchronizationContext()调程器的任务内部创建了另一个长时间运行的任务,UI将被冻结。更详细的解释可以在Task.Run0中找到

所以一般来说,如果你不使用嵌套的子任务,并且总是希望你的任务在线程池上执行,最好使用Task.Run,除非你有一些更复杂的场景。

除了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>>