异步方法中的奇怪调试器行为

Strange debugger behaviour in async method

当我越过代码中的断点时,我遇到了调试器的奇怪行为:

public async Task DoSomeWork()
{
     await Task.Run(() => { Thread.Sleep(1000); });

     var test = false;
     if (test)
     {
          throw new Exception("Im in IF body!");
     }
}

调试器进入 if body。值得注意的是,异常并没有真正被抛出,而只是看起来像被抛出。因此,如果将断点放在 throw 上,则无法重现。你必须把它放在上面然后下到 if body 才能抓住它。这同样适用于任何类型的异常实例(以及显式 null),甚至适用于 return 而不是 throw.

此外,即使我删除带有 await 的行,它也能正常工作。

我尝试 运行 来自不同 PC 的此代码片段,因此它不是 PC 问题。我还认为这是 VS 代码中的错误,并尝试在 JetBrains 的 Rider 中 运行 它 - 结果相同。

我确定这是异步的,但它是如何明确工作的?

您的代码使用 Visual Studio 2015 在 "Debug" 构建中轻松重现了该问题。我只需要添加 Program.Main() ,通过调用 DoSomeWork().Wait();,在方法中设置断点并单步执行它。

至于为什么会这样,这无疑是由于重写了async方法和生成调试数据库(.pdb)的结合。与迭代器方法类似,向方法中添加 async 会导致编译器将您的方法更改为状态机。生成的实际 IL 看起来与原始方法只有一点点相似。也就是说,如果您查看它,您可以识别原始代码的关键组件,但它现在位于一个大的 switch 语句中,该语句处理每个 [=15= 处的方法 returns 发生的事情] 语句,然后在每个等待的表达式完成时重新输入。

当程序语句出现在throw上时,实际上是在方法中隐含的return语句上。只是可执行文件的调试数据库没有为该行提供程序语句。

有一个提示,在调试时,这就是正在发生的事情。当您跨过 if 语句时,您会注意到它直接转到 throw 语句。如果 if 语句块 确实 被输入,下一个程序语句行实际上是该块的左大括号,而不是程序语句。

您还可以添加例如a Console.WriteLine() 在方法的末尾,这将为调试器提供足够的信息来同步,而不是在错误的行号处显示。

有关编译器如何处理 async 方法的更多信息,请参阅 Is the new C# async feature implemented strictly in the compiler 以及那里提供的链接(包括 Jon 关于该主题的系列文章)。