任务创建开销

时间:2019-01-09 10:21:35

标签: c# asynchronous task lazy-evaluation

我正在读一本书“ Terrell R.-.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;
}

作者说:

  

由于lambda表达式是异步的,因此可以在以下位置执行   任何调用Value的线程,该表达式将在   上下文。

据我了解,线程进入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操作,但是我们从Threadpool窃取了CPU线程。

3 个答案:

答案 0 :(得分:1)

  

由于lambda表达式是异步的,因此它可以在任何调用Value的线程上执行,并且该表达式将在上下文中运行。

lambda 可以从任何线程运行(除非您对允许访问Lazy的值的线程类型非常小心),因此它将运行在那个线程的上下文中。这不是因为它是异步的,即使它是同步的,也可以在任何调用它的线程的上下文中运行,这都是正确的。

  

据我了解,线程进入FetchPerson并停留在Lamda执行中。

lambda 是异步的,因此(如果实现正确)它将几乎立即返回。这就是异步的含义,因为它不会阻塞调用线程。

  

真的很糟糕吗?有什么后果?

如果错误地实现了异步方法,并使其长时间运行同步工作,那么是的,您正在阻塞该线程/上下文。如果您不这样做,那么您就不是。

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

  

作为解决方案,作者建议创建一个Task:[...]真的正确吗?

它不会破坏任何东西。它将安排工作在线程池线程中运行,而不是在原始上下文中运行。首先,这会产生一些额外的开销。您只需将ConfigureAwait(false)添加到您await的所有内容中,就可以以较低的开销完成大约相同的事情。

  

这是一个IO操作,但是我们从Threadpool窃取了CPU线程。

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

答案 1 :(得分:1)

确实,第一个访问Value的线程将执行lambda。 Lazy完全不知道异步和任务。它将运行该委托。

此示例中的委托将在调用线程上运行,直到命中await。然后它将返回一个Task,其中Task进入了惰性状态,此时惰性状态已完全完成。

该任务的其余部分将像其他任何任务一样运行。它将遵守SynchronizationContext发生时设置的TaskSchedulerawait(这是await行为的一部分)。实际上,这可能导致该代码在意外的上下文(例如UI线程)中运行。

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

这样做很好。它是解决实际问题的简便,可维护,低风险的解决方案。对于是否值得这样做有不同的意见。开销在所有可能的情况下都不重要。我个人非常喜欢这种代码。

如果您确定Value的所有调用方都在适当的上下文中运行,则不需要此方法。但是,如果您犯了一个错误,那将是一个严重的错误。因此,您可以辩称,最好在防守端插入Task.Run。务实,做有效的事。

还要注意,Task.Run是异步感知的(可以这么说)。它返回的任务实际上将解开内部任务(与Task.Factory.StartNew不同)。因此,像在此一样嵌套任务是安全的。

答案 2 :(得分:0)

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

作者似乎写了:

  

表达式将在上下文中运行

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