任务创建开销

Task creation overhead

我正在看书"Terrell R. - Concurrency in .NET"。

有一个很好的代码示例:

Lazy<Task<Person>> person = new Lazy<Task<Person>>(
     async () =>
     {
         using (var cmd = new SqlCommand(cmdText, conn))
         using (var reader = await cmd.ExecuteReaderAsync())
         {
             // some code...
         }
     });

async Task<Person> FetchPerson()
{
    return await person.Value;
}

作者说:

Because the lambda expression is asynchronous, it can be executed on any thread that calls Value, and the expression will run within the context.

据我了解,线程来到 FetchPerson 并卡在 Lamda 执行中。那真的很糟糕吗?什么后果?

作为解决方案,作者建议创建一个任务:

Lazy<Task<Person>> person = new Lazy<Task<Person>>(
      () => Task.Run(
        async () =>
        {
            using (var cmd = new SqlCommand(cmdText, conn))
            using (var reader = await cmd.ExecuteReaderAsync())
            {
                // some code...
            }
        }));

真的是这样吗?这是一个 IO 操作,但是我们从线程池中窃取了 CPU 个线程。

我完全不明白为什么 Terrell R. 建议使用 Task.Run。它没有任何附加值。在这两种情况下,lambda 都会被安排到线程池中。由于包含IO操作,线程池中的工作线程会在IO调用后被释放;当 IO 调用完成时,下一条语句将在线程池中的任意线程上继续。

作者好像是这样写的:

the expression will run within the context

是的,IO 调用的执行将在调用者的上下文中开始,但会在任意上下文中完成,除非您调用.ConfigureAwait

Because the lambda expression is asynchronous, it can be executed on any thread that calls Value, and the expression will run within the context.

lambda 可以来自任何线程运行(除非您注意让哪些类型的线程访问 Lazy 的值),因此在该线程的上下文中它将是 运行。那不是 因为它是异步的,即使它是同步的也是如此 运行 在任何线程碰巧调用它的上下文中。

As i understand it, the Thread come to FetchPerson and is stuck in Lamda execution.

lambda 是异步的,因此它将(如果实施得当)几乎立即 return。这就是异步的意思,因此它不会阻塞调用线程。

Is that realy bad? What consequences?

如果您错误地实现了您的异步方法,并让它做长时间的 运行 宁同步工作,那么是的,您正在阻止 thread/context。如果你不这样做,你就不是。

此外,默认情况下,异步方法中的所有延续都将在原始上下文中 运行(如果它有一个 SynchonrizationContext)。在您的情况下,您的代码几乎肯定不依赖于重用该上下文(因为您不知道调用者可能具有哪些上下文,我无法想象您编写了其余代码来使用它)。鉴于此,您可以对 await 的任何内容调用 .ConfigureAwait(false),这样您就不会将当前上下文用于这些延续。这只是一个小的性能改进,以便不浪费时间在原始上下文上安排工作,等待任何其他需要它的东西,或者让任何其他东西在不必要的时候等待这段代码。

As a solution, the author suggest to create a Task: [...] Is that really correct?

它不会破坏任何东西。它会将工作安排到线程池线程中的 运行,而不是原始上下文。这将有一些额外的开销开始。只需将 ConfigureAwait(false) 添加到您 await.

的所有内容,您就可以以较低的开销完成大致相同的事情

This is an IO operation, but we steal CPU thread from Threadpool.

该代码段将启动线程池线程上的 IO 操作。因为该方法仍然是异步的,所以它会在启动后立即将其 return 放入池中,并从池中获取一个新线程以在每次等待后再次启动 运行ning 。后者可能适用于这种情况,但是将启动初始异步操作的代码移动到线程池线程只会增加开销而没有任何实际价值(因为这是一个如此短的操作,您将花费更多的精力在线程上进行调度池线程不仅仅是 运行ning 它)。

第一个访问Value的线程确实会执行lambda。 Lazy 完全不知道异步和任务。它只会运行那个代表。

此示例中的委托将 运行 在调用线程上,直到 await 被命中。然后会return一个Task,即Task进入懒惰,至此懒惰完全完成。

该任务的其余部分将 运行 与任何其他任务一样。它将尊重在 await 发生时设置的 SynchronizationContextTaskScheduler(这是 await 行为的一部分)。这确实会导致该代码 运行ning 在意外的上下文中,例如 UI 线程。

Task.Run 是一种避免这种情况的方法。它将代码移动到线程池,为其提供特定的上下文。开销包括将工作排队到池中。池任务将在拳头await结束。所以这是 而不是 异步过同步。没有引入阻塞。唯一的变化是基于 CPU 的工作发生在哪个线程上(现在确定性地在线程池上)。

这样做就好了。它是针对实际问题的简单、可维护、低风险的解决方案。是否值得这样做,众说纷纭。开销很可能无关紧要。我个人非常同情这种代码。

如果您确定 Value 运行 的所有调用者都在合适的上下文中,那么您不需要这个。但如果你犯了一个错误,那就是一个严重的错误。所以你可以争辩说,最好是防御性地插入 Task.Run。务实做事。

另请注意,Task.Run 是异步感知的(可以这么说)。它 return 的任务本质上将解包内部任务(与 Task.Factory.StartNew 不同)。所以嵌套任务是安全的,就像这里完成的那样。