Task.Factory.StartNew不会等待任务完成吗?

7
我发现,如果我使用以下实现,下面的代码实际上不会等待任务 client.SendAsync() 的完成:
taskList.Add(Task.Factory.StartNew(() => new Program().Foo()));

如果我把Task.Factory.StartNew()改成new Program().Foo()或者Task.Run(() => new Program.Foo(),它们都可以正确地输出一些信息。这两种方法有什么区别?
internal class Program
{
    private async Task Foo()
    {
        while (true)
        {
            var client = new HttpClient();
            var requestMessage = new HttpRequestMessage(HttpMethod.Head, "http://www.google.com");
            HttpResponseMessage response = await client.SendAsync(requestMessage);
            Console.WriteLine(response.RequestMessage.RequestUri.ToString());
        }
    }

    private static void Main(string[] args)
    {
        var taskList = new List<Task>();

        // This won't output anything.
        taskList.Add(Task.Factory.StartNew(() => new Program().Foo()));

        // This will.
        taskList.Add(Task.Run(() => new Program().Foo()));

        // So does this.
        taskList.Add(new Program().Foo());

        Task.WaitAll(taskList.ToArray());
    }
}

根据这篇MSDN文章,似乎Task.Run(someAction);等同于Task.Factory.StartNew(someAction, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);。但即使我将代码更改为此,也不会输出任何内容。为什么呢?
internal class Program
{
    private async Task Foo()
    {
        while (true)
        {
            var client = new HttpClient();
            var requestMessage = new HttpRequestMessage(HttpMethod.Head, "http://www.google.com");
            HttpResponseMessage response = await client.SendAsync(requestMessage);
            Console.WriteLine(response.RequestMessage.RequestUri.ToString());
        }
    }

    private static void Main(string[] args)
    {
        var taskList = new List<Task>();
        taskList.Add(Task.Factory.StartNew(() => new Program().Foo(), CancellationToken.None,
            TaskCreationOptions.DenyChildAttach, TaskScheduler.Default));
        //taskList.Add(Task.Run(() => new Program().Foo()));
        //taskList.Add(new Program().Foo());
        Task.WaitAll(taskList.ToArray());
    }
}

主题中的问题的答案是:不,除非你要求。 - Arghya C
看一下async/await模式。这将扩展你对该问题的视野。 - Larry
2
@derekhh:阅读你的整个参考文献。你引用的部分前缀为当你将Action传递给Task.Run,而你的代码并没有这样做。在博客文章的后面,他解释了Task.Run如何与异步委托不同。 - Stephen Cleary
3个回答

19
问题在于 Task.Factory.StartNew 不是 "任务感知" 的。也就是说,从您对 StartNew 方法调用的返回类型实际上是一个 Task<Task>。这意味着您只等待 "外部任务" 完成,而不是 "内部任务"。
解决该问题的简单方法是使用 TaskExtensions.Unwrap() 方法:
private static void Main(string[] args)
{
    var taskList = new List<Task>();
    taskList.Add(Task.Factory.StartNew(() => new Program().Foo()).Unwrap());

    Task.WaitAll(taskList.ToArray());
}

Task.Run 之所以有效,是因为它具有“任务感知”能力。 它有一个重载,接受一个 Func<Task>,内部会自动调用 Unwrap 方法,只返回内部的任务。


1
对于单个任务:var task = Task.Factory.StartNew(something); task.Wait();请执行以上代码。 - vighnesh153

3

Task.Factory.StartNew默认情况下不会等待内部任务完成。如果您使用返回值创建一个变量,您将得到以下结果:

Task<Task> task = Task.Factory.StartNew(() => new Program().Foo());

这意味着委托会在内部任务遇到 await 语句后立即返回。您需要调用 UnWrap 方法来强制异步执行:

Task task = Task.Factory.StartNew(() => new Program().Foo()).Unwrap();
taskList.Add(task);

你发布的博客文章还解释了同步和异步委托之间的区别。

1

您的代码使用语句Task.WaitAll(taskList.ToArray());等待new Program().Foo()结束。

然而,Foo在返回之前不会等待client.SendAsync(requestMessage)的结束,因为它有一个await。这个await将让方法返回Task对象,并允许线程回到下一行。

"问题"是,由于您在await上返回,调用Foo方法的任务在此时已经完成,甚至在命令行输出之前(Foo方法返回的Task对象没有完成)。

您基本上有3个"时间线"(我不会说线程,因为我不确定HttpClient实际上是否启动了新线程,但如果有人可以确认这一点,我会编辑帖子)和2个任务:

  • 您的Main线程
  • 使用StartNew创建的任务
  • SendAsync任务。
您的Main线程正在等待由StartNew创建的任务结束,但该任务不等待SendAsync调用的结束,因此它不等待命令行输出以将其视为已完成。由于只有那个任务被考虑在WaitAll中,而SendAsync没有被考虑在内,所以它早早地返回是正常的。
要实现您想要的行为,您应该像这样做:
taskList.Add(Task.Factory.StartNew(() => new Program().Foo().Wait()));

但是,开启一个新的任务只为等待另一个任务完成并不太合理,所以最好使用taskList.Add(new Program().Foo());


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接