在一个方法上使用 await Task.Factory.StartNew 会立即返回

8
我正在运行以下代码(C#7.1控制台应用程序),但我无法真正理解为什么会有不同的行为。
如果我等待常规异步方法调用或Task.Run,则它将按预期工作(即应用程序不会立即返回)。但是,如果我使用Task.Factory.StartNew,则它将立即返回而不实际运行代码。
奇怪的是 - 如果我在方法中使用StartNew但删除await,则它将不会立即返回...
问题:这会立即返回:
static async Task Main(string[] args)
{
    await Task.Factory.StartNew(DisplayCurrentInfo);
}

static async Task DisplayCurrentInfo()
{
    await WaitAndApologize();
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Thread.Sleep(3000);
}

即 - 我将无法看到任何打印到控制台的内容,而且控制台已经被关闭。

没问题:这不会立即返回

static async Task Main(string[] args)
{
    await DisplayCurrentInfo(); // or await Task.Run(DisplayCurrentInfo);
}

static async Task DisplayCurrentInfo()
{
    await WaitAndApologize();
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Thread.Sleep(3000);
}

奇怪:这也不会立即返回:
static async Task Main(string[] args)
{
    await Task.Factory.StartNew(DisplayCurrentInfo); 
}

static async Task DisplayCurrentInfo()
{
    WaitAndApologize();
    Console.WriteLine($"The current time is {DateTime.Now.TimeOfDay:t}");
    Thread.Sleep(3000);
}

等待并道歉:

static async Task WaitAndApologize()
{
    // Task.Delay is a placeholder for actual work.  
    await Task.Delay(2000);
    // Task.Delay delays the following line by two seconds.  
    Console.WriteLine("\nSorry for the delay. . . .\n");
}

3
“立即返回”是指“立即返回尚未完成的任务”,对吗?如果是这样,那就是预期的和设计好的。如果不是这个意思,请澄清一下。你会得到已完成的任务作为回报吗? - Lasse V. Karlsen
3
不要将任务嵌套在任务中,你需要使用双重等待。你正在使用 Task.Run 创建一个名为 A 的任务,目的是创建另一个任务 B,然后你在等待 A,但没有等待 B。除非你知道如何正确地执行此操作,请不要使用该调用语法,只需使用有效的方法。 - Lasse V. Karlsen
1
一个快速的解决方案,如果你没有理解我的第二条评论,可能会让你有更多的问题,那就是使用双重等待:await await Task.Factory....,但是一个更好的解决方案是只使用 await DisplayCurrentInfo(); - Lasse V. Karlsen
1
一旦你开始编写async代码,任何通过RunStartNew手动构建Task的方式都可能是可疑的。你的async方法已经创建了Task - 为什么还需要更多的呢? - Damien_The_Unbeliever
1
如果你在彼此包含的足够多的任务中编写所有等待,await X 将给出一个新表达式,如果这个表达式是具有等待器的东西,那么你也可以等待它,如此无限循环。任务内嵌。 - Lasse V. Karlsen
显示剩余2条评论
3个回答

13
如果你使用Task.Factory.StartNew(MethodThatReturnsTask),你将得到一个Task<Task<T>>或者Task<Task>,具体取决于该方法是否返回一个通用任务。
最终的结果是你会有两个任务:
1. Task.Factory.StartNew产生一个调用MethodThatReturnsTask的任务,我们称这个任务为“任务A”。 2. 在你的情况下,MethodThatReturnsTask返回一个Task,我们称之为“任务B”。这意味着使用了一个处理它的StartNew重载,最终的结果是你得到一个包装了Task B的Task A。
要“正确地”等待这些任务需要两个awaits,而不是一个。你的单个await仅等待Task A,这意味着当它返回时,Task B仍在执行等待完成。
为了幼稚地回答你的问题,请使用两个awaits:
await await Task.Factory.StartNew(DisplayCurrentInfo);

然而,值得怀疑的是,为什么需要生成一个任务来启动另一个异步方法。相反,您最好使用第二种语法,只需等待该方法:

await DisplayCurrentInfo();

观点如下:通常来说,一旦你开始编写异步代码,使用Task.Factory.StartNew或其任何类似方法应该被保留用于当您需要生成一个线程(或类似的东西)来调用不是异步的内容与其他东西并行。如果您不需要这种特定模式,则最好不要使用它。


1
不要使用 await await,我会对第一个任务进行 Unwrap。但是我同意你的总体结论,如果不需要就不要使用 StartNew。 - Ronald
1
啊...我现在明白了。虽然你没有回答“奇怪”的情况,但是VibeeshanRC在下面解释了。谢谢! - Maverick Meerkat

2
当你调用 Task.Factory.StartNew(DisplayCurrentInfo); 时,你正在使用以下签名:
public Task<TResult> StartNew<TResult>(Func<TResult> function)

现在,由于您传递了一个签名为Task DisplayCurrentInfo()的方法,因此TResult类型为Task

换句话说,您从Task.Factory.StartNew(DisplayCurrentInfo)返回了一个Task<Task>,然后等待外部任务返回Task - 但是您没有等待它。因此,它立即完成。


2
await Task.Factory.StartNew(DisplayCurrentInfo);

第一个示例会立即返回,因为您正在创建一个新任务并等待它,这将调用另一个任务,但不会等待它。考虑使用类似下面的伪代码来保持等待状态。
await Task.Factory.StartNew( await DisplayCurrentInfo); //Imaginary code to understand

在第三个例子中,WaitAndApologize 没有被 awaited,但是它被线程休眠(整个线程被阻塞)了。
只通过代码,我们无法确认 Task 工厂是创建了一个新的线程还是在现有线程上运行。如果在同一线程上运行,则整个线程会被阻塞,因此您会感觉代码在await Task.Factory.StartNew(DisplayCurrentInfo);处等待,但实际上并没有发生。
如果在不同的线程上运行,您将不会得到与上述相同的结果。

1
如果您要重写第二段代码以实际编译,您需要编写一个异步lambda表达式或匿名方法,在这种情况下,您现在有三个任务,并且仍然需要外部的双重等待。 - Lasse V. Karlsen
加一并感谢您对这个奇怪情况的解释。我现在明白了。 - Maverick Meerkat

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