为何Task.Run与Task.Factory.StartNew在await调用时行为不同?
我原本以为Task.Run是Task.Factory.StartNew的更新且推荐的替代方案,二者行为一致,但在以下代码中发现了行为差异。我创建了一个简单的.NET 6控制台应用:
Worker.cs 代码
internal class Worker { private readonly HttpClient httpClient = new(); public async Task StartTaskRun(CancellationToken token) { await Task.Run(async () => await SomeWork(token)); } public async Task StartTaskFactory(CancellationToken token) { await Task.Factory.StartNew(async () => await SomeWork(token)); } private async Task SomeWork(CancellationToken token) { try { while (true) { var result = await httpClient.GetStringAsync("https://www.yahoo.com", token); await Task.Delay(5000, token); } } catch (TaskCanceledException) { Console.WriteLine("Task has been canceled"); } } }
Program.cs 代码
var cancellationTokenSource = new CancellationTokenSource(); var worker = new Worker(); //worker.StartTaskFactory(cancellationTokenSource.Token); // 1# 返回 //worker.StartTaskRun(cancellationTokenSource.Token); // 2# 返回 //await worker.StartTaskFactory(cancellationTokenSource.Token); // 3# 返回 //await worker.StartTaskRun(cancellationTokenSource.Token); // #4 不返回 Console.WriteLine("Hello world"); Console.ReadLine(); cancellationTokenSource.Cancel(); Console.WriteLine("Cancelling"); Console.ReadLine();
在Program.cs中,四种调用启动方法的方式里:1#和2#是“即发即弃”调用,控制台正常输出“Hello World”并继续执行逻辑;但3#和4#结果不同:3#与1#、2#一样正常返回,4#却似乎一直在等待StartTaskRun()调用。
差异原因解析
核心差异在于Task.Factory.StartNew不会自动解包异步lambda的嵌套Task,而Task.Run会:
Task.Factory.StartNew的行为
当你调用Task.Factory.StartNew(async () => await SomeWork(token))时,传入的异步lambda本质是Func<Task>类型,因此StartNew返回的是Task<Task>——外层Task代表“启动lambda执行”这个操作,内层Task才是SomeWork的异步任务。
当你await这个Task<Task>时,只会等待外层Task完成:外层Task在lambda执行到第一个await(httpClient.GetStringAsync)时就会完成,因为此时lambda会返回一个未完成的内层Task,外层任务的使命已经完成,所以await会立即结束,代码继续执行Console.WriteLine("Hello world")。Task.Run的行为
Task.Run专门提供了针对Func<Task>的重载,它会自动解包嵌套的Task,直接返回lambda内部的SomeWork任务。
因此await Task.Run(...)实际上是在等待整个SomeWork的异步流程完成,而SomeWork是一个无限循环(直到取消令牌触发),所以await会一直阻塞,直到调用cancellationTokenSource.Cancel()才会结束。
如果要让Task.Factory.StartNew达到和Task.Run一致的等待效果,需要手动调用Unwrap()方法解包:
await Task.Factory.StartNew(async () => await SomeWork(token)).Unwrap();
内容的提问来源于stack exchange,提问作者millie

