【问题标题】:Achieving async performance advantages in synchronous code bases在同步代码库中实现异步性能优势
【发布时间】:2021-01-18 19:31:07
【问题描述】:

我有一个非常大的同步 Web 应用程序,它可以从异步调用中显着受益,但重构它以实现这一点需要大量工作。

在应用程序中有一个特定的点,其中进行了 2 次昂贵的远程调用,并且能够同时触发这两个调用将大大提高性能。显然流程是同步的,所以我使用Task.Run来实现异步调用:

private void RemoteCalls()
{
    var task1 = Task.Run(async() => await DoExpensiveThing());
    var task2 = Task.Run(async () => await DoAnotherExpensiveThing());
    
    Task.WhenAll(task1, task2);
}

private async Task DoExpensiveThing()
{
    await //some expensive call
}

private async Task DoAnotherExpensiveThing()
{
    await //another expensive call
}

这成功实现了我所追求的,两个昂贵调用的工作同时执行并等待两者完成。

我的问题是,这样做有什么明显的缺点吗?我有一种(模糊的)感觉 async 真的被设计用于从上到下一直异步的应用程序。由于使用 Task.Run,​​在升级应用程序之前,我会因为使用上述内容而受苦吗?

【问题讨论】:

  • 因为你没有使用awaitTask.WhenAll(task1, task2); 你的程序会有问题。如果您需要在同步上下文中运行async 代码,您仍然需要await 最终结果 - 您可以使用线程池和经典同步原语(如ManualResetEvent)和“非阻塞”轮询循环以确保安全。
  • 首先,异步和并发是不同的东西;听起来你真的很追求并发。请注意,Task.WhenAll 可能不是您真正想要的,因为它 返回一个必须等​​待的任务 - 但除非您处于异步上下文中,否则您不能等待,这意味着你实际上需要阻止 (Wait()),这是一个非常糟糕的主意。
  • @FBryant87 不幸的是,事情从来没有这么简单。幸运的是,ASP.NET WebForms 有它自己的“后台工作者”功能 (hostingenvironment.queuebackgroundworkitem),您应该使用它。
  • @PeterCsala 因为这要求父方法也是async OP 说他们不能这样做。

标签: c# .net asynchronous


【解决方案1】:

因为您没有将awaitTask.WhenAll(task1, task2); 一起使用,您的程序出现问题。如果您需要在同步上下文中运行异步代码,您仍然需要await 最终结果 - 您可以使用线程池和经典同步原语(如ManualResetEvent)和“非阻塞”轮询循环为了安全。

您似乎想要同时运行两个方法 - 这些方法可能是也可能不是“真正的”异步(即受 IO 限制,而不是受 CPU 限制)。在这种情况下,通过Task.Run 使用线程池,但“外部”任务本身需要在池中运行,以便Task 调度程序。

而不是简单地阻塞Task.Result,我更喜欢使用Task.Wait(Int32)(而不是Task.Wait()!)循环轮询它,这可以缓解一些死锁问题 - 请注意,这与.Result的问题相同strong>非常低效地使用程序线程。这种方法不应该用于高性能、高需求或高吞吐量的应用程序中,尤其是在 ASP.NET WebForms 中(现在已经过时且可怕)“每个请求一个线程”模型仍在使用中:


// NASAL DEMON WARNING:
// This method is only intended as a stopgap as this approach does not scale to handling many concurrent entrypoint calls: so it should not be used in *serious business* applications. 
private void EntrypointThatWillBlock()
{
    Task threadPoolTask = Task.Run( DoConcurrentRequestsAsync );
    
    Boolean didComplete = threadPoolTask.Wait( millisecondsTimeout: 30 * 1000 );
    if( !didComplete ) throw new TimeoutException( "Didn't complete in 30 seconds." );
}

private async Task DoConcurrentRequestsAsync()
{
    try
    {
        Task task1 = this.DoExpensieThingAsync();
        Task task2 = this.DoAnotherExpensiveThing();

        await Task.WhenAll( task1, task2 ).ConfigureAwait(false);
    }
    catch( Exception ex )
    {
        // You should log any errors here (and then re-throw) as dealing with unhandled exceptions in async contexts can be a mess. At least by logging here you're guaranteed to get _something_ logged.
        this.log.LogError( ex );
        throw; // <-- Don't use `throw ex;` as that resets the stack-trace)
    }
}

免责声明:通过使用特定于上下文的同步上下文,可能有更好的方法来做到这一点。当您使用 ASP.NET WebForms 您可以正确使用 async 并进行一些配置调整(check your web.config file,如果适用,将 Async="true" 添加到您的 @Page 指令中,使用 IHttpAsyncHandler 和/或HostingEnvironment.QueueBackgroundWorkItem等等)。

另请阅读What's the meaning of "UseTaskFriendlySynchronizationContext"?

【讨论】:

  • 恐怕没有系统的技术和商业背景,就不可能做出这样的陈述
  • @FBryant87 这是真的!但我不会让 事实理性和知情的讨论 妨碍我告诉人们在互联网上应该做什么 :)
  • If whoever downvoted me is reading this, please post a comment reply with feedback for improving my answer 恐怕有很多人会停留在“task.Wait = bad”上,而不去想它是否是一个合法的用例。我只想指出,如果您的应用程序正在处理大量请求(每秒数百个请求),您可能会遇到线程池饥饿:medium.com/criteo-engineering/… 不幸的是,我认为没有任何灵丹妙药可以避免这种情况
  • 哦,还有一件事@FBryant87。 Task.Run 不流动同步上下文。这是避免死锁的好方法但是您不能在同步上下文之外使用HttpContext。我怀疑你在那种情况下是否需要它,但请记住这一点。
  • 嗨戴。我是反对者,原因是我看不到您的回答如何解决 OP 的问题。 OP 问道:“我的问题是,这样做有什么明显的缺点吗?[...] 我会因为使用上述 [...] 而受苦吗?” 简而言之OP 要求对他们自己的方法进行评估,而不是提供有关更好解决方案的建议。就个人而言,我无法判断您的建议是否比 OP 的方法更好,主要是因为我很惊讶 OP 的方法完全有效(他们声称它成功地实现了他们所追求的)。
猜你喜欢
  • 1970-01-01
  • 2020-11-05
  • 1970-01-01
  • 1970-01-01
  • 2016-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多