【问题标题】:JetBrains dotTrace: confusing `await` time for async function callsJetBrains dotTrace:混淆异步函数调用的“等待”时间
【发布时间】:2022-02-11 07:10:35
【问题描述】:

在使用 dotTrace 分析使用 async/await 功能的应用程序时,我注意到父方法和子方法的 await 时间存在一些不一致。让我们考虑这个例子:

static async Task Main()
{
    await ChildMethod();
}

static async Task ChildMethod()
{
    await SomeOperationAsync();
}

有趣的是Main 方法的await 时间可能(甚至小于 await 时间@987654329 @ 这对我来说看起来很奇怪,因为直观上 await 父母的时间应该至少作为await 孩子的时间。

对于实数,让我们回到前面的例子,假设SomeOperationAsyncTask.Delay(n)。因此,父级的await 时间将是xx 是针对.NET 6.0 的应用程序的~n 和针对.NET Core 3.1 的应用程序的<n)和子级的await 时间将是~2*x

这是来自 dotTrace 的屏幕截图,用于了解它的外观(针对面向 .NET Core 3.1 的应用和Task.Delay(800)):

  1. 这是await 方法Main 的时间。 567ms 对于await ChildMethod(),我希望这是~800,因为 ChildMethod 等待 Task.Delay(800)
  2. 这是await 方法ChildMethod 的时间。对于await Task.Delay(800)1140 毫秒。我也希望这是~800

可能我误解了 dotTrace 中的 await 时间是什么,所以如果是,请纠正我。我将其理解为安排任务所需的时间 + 实际任务执行

如果有人知道这种行为是否是预期的并能解释如何分析它,那就太好了。

我在 dotTrace 2021.3.3 中使用时间线查看器。

更新

我想我理解了我困惑的第一部分,即为什么 await 时间小于 Main 方法中传递给 Task.Delay 的时间。这是因为任务的计时器比继续附加(内部调用AwaitUnsafeOnCompleted)更早开始,因此它只等待剩余时间。在我的情况下,这个调用大约需要 300ms,因此 await 时间变为 time passed to Task.Delay - time spent on attaching continuation

【问题讨论】:

    标签: c# async-await profiling jetbrains-ide dottrace


    【解决方案1】:

    我认为您误解了 async/await 在 dotnet 中的工作方式。你可以在微软官方文档中查看:https://docs.microsoft.com/es-es/dotnet/csharp/language-reference/operators/await

    基本上,当您执行异步代码(返回任务的方法)时,该方法的执行将发生在独立于原始线程的另一个线程中,当您需要该任务的结果时,原始线程将等待直到完成的第二个线程来检索结果。您可以在他们的文档中阅读:

    await 运算符不会阻塞计算异步方法的线程。当 await 操作符挂起封闭的 async 方法时,控制权返回给方法的调用者。

    看这个例子

    public async Task<Person> Main()
    {
        string name = await GetName(personId);
        int age = await GetAge(personId);
        return new Person(name, age)
    }
    
    public async Task<string> GetName(int personId)
    {
        //imagine you have a HttpClient in this class that calls an API to get people info
        return _httpClient.GetAsync($"https://someurl.com/getName/{personId}");
    }
    
    public async Task<int> GetAge(int personId)
    {
        return _httpClient.GetAsync($"https://someurl.com/getAge/{personId}");
    }
    

    这样主执行线程将创建另外两个线程来获取年龄信息和personId的名称,并且直到该线程需要检索其他两个线程的一些信息时才会停止(返回一个新人的行) 因此,您的代码中发生的情况是,由于没有使用该异步操作的结果,它只是继续原始线程而不停止,因此具有延迟的异步任务持续时间比原始线程长是正常的。

    【讨论】:

    • 感谢您的回复,但是。 “该方法的执行将发生在独立于原始线程的另一个线程中” - 这是不正确的,该方法的执行将在调用线程上,直到遇到awaitcontinuationawait之后的逻辑)可能在另一个线程(线程池的线程)上执行
    • 这个“原来的线程会等到第二个线程完成取回结果”好像也有错误。在异步世界中,什么都不等待,一旦异步操作完成,延续将被安排在线程池线程上(如果它没有立即完成,在这种情况下,原始调用线程将执行延续逻辑)。
    • 不幸的是,答案没有说明为什么Main 方法的await 时间小于800 以及为什么ChildMethod 的等待时间大于800 并大于@ 987654331@Main方法的时间。或者至少我不明白这一点(如果是这样,我很抱歉)。您能否详细说明我的代码中的“没有使用异步操作的结果”以及“延迟的任务持续时间比原始线程长”是什么意思?它们之间有什么关系?
    • “这样主执行线程将创建另外两个线程来获取年龄信息和那个人的名字”我不认为这是真的。 main 线程不创建线程,它只是执行返回任务的GetName 方法,然后由于await 主线程返回给调用者(不确定控制台应用程序中的调用者是什么),然后一旦数据准备好,继续(调用GetAge)将被安排在线程池的线程 或工作线程上。我在这里的解释中遗漏了很多细节,但这是一般的想法。
    • 这个答案显示了对异步编程的严重误解。您混淆了并行和异步。并行意味着同时运行代码的两部分,这需要多个线程。这是关于您的代码如何运行。异步意味着在当前线程等待外部事物(例如 I/O 请求)时释放当前线程。这是关于您的代码如何等待。当你异步等待时,there is no thread.
    猜你喜欢
    • 2015-11-06
    • 2019-01-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-14
    • 2021-06-14
    相关资源
    最近更新 更多