C#async /等待控制台应用程序中的奇怪行为

我构建了一些异步/等待演示控制台应用程序并得到奇怪的结果。 码:

class Program { public static void BeginLongIO(Action act) { Console.WriteLine("In BeginLongIO start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); Thread.Sleep(1000); act(); Console.WriteLine("In BeginLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); } public static Int32 EndLongIO() { Console.WriteLine("In EndLongIO start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); Thread.Sleep(500); Console.WriteLine("In EndLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); return 42; } public static Task LongIOAsync() { Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); var tcs = new TaskCompletionSource(); BeginLongIO(() => { try { tcs.TrySetResult(EndLongIO()); } catch (Exception exc) { tcs.TrySetException(exc); } }); Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); return tcs.Task; } public async static Task DoAsync() { Console.WriteLine("In DoAsync start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); var res = await LongIOAsync(); Thread.Sleep(1000); Console.WriteLine("In DoAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); return res; } static void Main(String[] args) { ticks = DateTime.Now.Ticks; Console.WriteLine("In Main start... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); DoAsync(); Console.WriteLine("In Main exec... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); Thread.Sleep(3000); Console.WriteLine("In Main end... \t\t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); } private static Int64 ticks; } 

结果如下:

在此处输入图像描述

也许我不完全明白究竟是什么等待。 我想如果执行等待,那么执行将返回到调用方法和等待在另一个线程中运行的任务。 在我的示例中,所有操作都在一个线程中执行,并且执行不会在await关键字之后返回到调用方法。 真相在哪里?

这不是async-await工作原理。

将方法标记为async不会创建任何后台线程。 当您调用async方法时,它会同步运行,直到异步点,然后才返回调用方。

异步点就是await尚未完成的任务。 当它完成时,计划执行该方法的其余部分。 此任务应表示实际的异步操作(如I / O或Task.Delay )。

在你的代码中没有异步点,没有返回调用线程的点。 线程越来越深,并且在Thread.Sleep阻塞,直到这些方法完成并且DoAsync返回。

举个简单的例子:

 public static void Main() { MainAsync().Wait(); } public async Task MainAsync() { // calling thread await Task.Delay(1000); // different ThreadPool thread } 

这里我们有一个实际的异步点( Task.Delay ),调用线程返回Main ,然后在任务上同步阻塞。 一秒钟后, Task.Delay任务完成,其余的方法在不同的ThreadPool线程上执行。

如果不使用Task.Delay我们将使用Thread.Sleep那么它将在同一个调用线程上运行。

要真正理解这种行为,您需要首先了解什么是Task ,以及asyncawait实际对您的代码做了什么。

Task是“活动”的CLR表示。 它可以是在工作池线程上执行的方法。 它可以是通过网络从数据库中检索某些数据的操作。 它的通用性允许它封装许多不同的实现,但从根本上说,你需要理解它只是意味着“一个活动”。

Task类为您提供了检查活动状态的方法:是否已完成,是否尚未启动,是否生成错误等。此活动建模使我们能够更轻松地构建构建为一系列活动,而不是一系列方法调用。

考虑这个简单的代码:

 public void FooBar() { Foo(); Bar(); } 

这意味着“执行方法Foo ,然后执行方法Bar 。如果我们考虑从FooBar返回Task的实现,这些调用的组成是不同的:

 public void FooBar() { Foo().Wait(); Bar().Wait(); } 

现在意思是“使用Foo方法启动任务并等待它完成,然后使用方法Bar启动任务并等待它完成。” 在Task上调用Wait()很少是正确的 – 它导致当前线程阻塞直到Task完成并且可能在一些常用的线程模型下导致死锁 – 所以我们可以使用asyncawait来实现类似的效果而不会产生危险呼叫。

 public async Task FooBar() { await Foo(); await Bar(); } 

async关键字导致您的方法的执行被分解为块:每次编写await ,它都会获取以下代码并生成“continuation”:在等待任务完成后作为Task执行的方法。

这与Wait()不同,因为Task没有链接到任何特定的执行模型。 如果从Foo()返回的Task表示通过网络进行的调用,则没有线程被阻塞,等待结果 – 有一个Task等待操作完成。 当操作完成时, Task被安排执行 – 这个调度过程允许将活动的定义与执行它的方法分开,并且是使用任务的能力。

因此,该方法可以概括为:

  • 启动任务Foo()
  • 当该任务完成时,启动任务Bar
  • 当该任务完成时,表明方法任务已完成

在您的控制台应用程序中,您不等待任何代表挂起IO操作的Task ,这就是您看到被阻塞线程的原因 – 从来没有机会设置一个异步执行的延续。

我们可以使用Task.Delay()方法修复LongIOAsync方法,以异步方式模拟长IO。 此方法返回在指定时间段后完成的Task 。 这为我们提供了异步延续的机会。

 public static async Task LongIOAsync() { Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); await Task.Delay(1000); Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); } 

简短的回答是LongIOAsync()是阻塞的。 如果你在GUI程序中运行它,你实际上会看到GUI暂时冻结 – 而不是async / await应该工作的方式。 因此整个事情都崩溃了。

您需要在Task中包装所有长时间运行的操作,然后直接等待该Task。 在那期间什么都不应该阻止。

实际在后台线程上运行某些东西的行是

  Task.Run( () => { } ); 

在您的示例中,您不是在等待Task而是TaskCompletionSource的Task

  public static Task LongIOAsync() { var tcs = new TaskCompletionSource(); Task.Run ( () => BeginLongIO(() => { try { tcs.TrySetResult(EndLongIO()); } catch (Exception exc) { tcs.TrySetException(exc); } })); return tcs.Task; } 

当等待LongIOAsync时,你正在等待来自tcs的任务女巫是从给予Task.Run()的委托中的后台线程设置的

应用此更改:

  public static Task LongIOAsync() { Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); var tcs = new TaskCompletionSource(); Task.Run ( () => BeginLongIO(() => { try { tcs.TrySetResult(EndLongIO()); } catch (Exception exc) { tcs.TrySetException(exc); } })); Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks) / TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); return tcs.Task; } 

或者在这种情况下,您可以等待从Task.Run()返回,TaskCompletionSource适用于您希望传递将Task设置为完整或其他方式的情况。