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
,以及async
和await
实际对您的代码做了什么。
Task
是“活动”的CLR表示。 它可以是在工作池线程上执行的方法。 它可以是通过网络从数据库中检索某些数据的操作。 它的通用性允许它封装许多不同的实现,但从根本上说,你需要理解它只是意味着“一个活动”。
Task
类为您提供了检查活动状态的方法:是否已完成,是否尚未启动,是否生成错误等。此活动建模使我们能够更轻松地构建构建为一系列活动,而不是一系列方法调用。
考虑这个简单的代码:
public void FooBar() { Foo(); Bar(); }
这意味着“执行方法Foo
,然后执行方法Bar
。如果我们考虑从Foo
和Bar
返回Task
的实现,这些调用的组成是不同的:
public void FooBar() { Foo().Wait(); Bar().Wait(); }
现在意思是“使用Foo
方法启动任务并等待它完成,然后使用方法Bar
启动任务并等待它完成。” 在Task
上调用Wait()
很少是正确的 – 它导致当前线程阻塞直到Task
完成并且可能在一些常用的线程模型下导致死锁 – 所以我们可以使用async
和await
来实现类似的效果而不会产生危险呼叫。
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设置为完整或其他方式的情况。