异步和等待:他们不好吗?

时间:2014-02-14 05:02:21

标签: performance asp.net-mvc-4 async-await

我们最近开发了一个基于SOA的站点,但该站点在负载不足时最终出现了可怕的负载和性能问题。我在这里发布了一个与此问题相关的问题:

ASP.NET website becomes unresponsive under load

该站点由一个API(WEB API)站点组成,该站点托管在一个4节点集群和一个托管在另一个4节点集群上的网站上,并调用该API。两者都是使用ASP.NET MVC 5开发的,所有操作/方法都基于async-await方法。

在一些监控工具(如NewRelic)下运行站点,调查几个转储文件并分析工作进程后,结果发现在非常轻的负载下(例如16个并发用户),我们最终拥有大约900个线程,其中使用了100个CPU的百分比并填满了IIS线程队列!

尽管我们设法通过引入大量缓存和性能修正将网站部署到生产环境,但我们团队中的许多开发人员认为我们必须删除所有异步方法并将API和网站转换为普通的Web API和简单地返回Action结果的Action方法。

我个人对方法不满意,因为我的直觉是我们没有正确使用异步方法,否则就意味着微软已经推出了一个基本上具有相当破坏性且无法使用的功能!

您是否知道有哪些参考资料清除了应该/可以使用异步方法的位置和方式?我们应该如何使用它们来避免这种戏剧?例如基于我在MSDN上阅读的内容,我相信API层应该是异步的,但网站可能是一个普通的无异步ASP.NET MVC站点。

更新

以下是与API进行所有通信的异步方法。

public static async Task<T> GetApiResponse<T>(object parameters, string action, CancellationToken ctk)
{
        using (var httpClient = new HttpClient())
        {
            httpClient.BaseAddress = new Uri(BaseApiAddress);

            var formatter = new JsonMediaTypeFormatter();

            return
                await
                    httpClient.PostAsJsonAsync(action, parameters, ctk)
                        .ContinueWith(x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result, ctk);
        }
    }

这种方法有什么愚蠢的吗?请注意,当我们将所有方法转换为非异步方法时,我们获得了更好的性能。

这是一个示例用法(我已经删除了与验证,日志记录等相关的代码的其他位。此代码是MVC操作方法的主体。)

在我们的服务包装中:

public async static Task<IList<DownloadType>> GetSupportedContentTypes()
{
  string userAgent = Request.UserAgent;
  var parameters = new { Util.AppKey, Util.StoreId, QueryParameters = new { UserAgent = userAgent } };
  var taskResponse = await  Util.GetApiResponse<ApiResponse<SearchResponse<ProductItem>>>(
                    parameters,
                    "api/Content/ContentTypeSummary",
                    default(CancellationToken));
                    return task.Data.Groups.Select(x => x.DownloadType()).ToList();
 }

在行动中:

public async Task<ActionResult> DownloadTypes()
    {
        IList<DownloadType> supportedTypes = await ContentService.GetSupportedContentTypes();

3 个答案:

答案 0 :(得分:35)

  

这种方法有什么愚蠢的吗?请注意,我们转换时   对于非异步方法的所有方法,我们都获得了更好的性能。

我可以看到至少有两件事情出现了问题:

public static async Task<T> GetApiResponse<T>(object parameters, string action, CancellationToken ctk)
{
        using (var httpClient = new HttpClient())
        {
            httpClient.BaseAddress = new Uri(BaseApiAddress);

            var formatter = new JsonMediaTypeFormatter();

            return
                await
                    httpClient.PostAsJsonAsync(action, parameters, ctk)
                        .ContinueWith(x => x.Result.Content
                            .ReadAsAsync<T>(new[] { formatter }).Result, ctk);
        }
    }

首先,你传递给ContinueWith的lambda是阻塞的:

x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }).Result

这相当于:

x => { 
    var task = x.Result.Content.ReadAsAsync<T>(new[] { formatter });
    task.Wait();
    return task.Result;
};

因此,您正在阻止正在执行lambda的池线程。这有效地消除了自然异步ReadAsAsync API的优势,并降低了您的可扩展性网络应用。在你的代码中注意这样的其他地方。

其次,ASP.NET请求由服务器线程处理,其上安装了特殊的同步上下文AspNetSynchronizationContext。当您使用await进行延续时,将继续回调发布到同一个同步上下文,编译器生成的代码将处理此问题。 OTOH,当你使用ContinueWith时,这不会自动发生。

因此,您需要显式提供正确的任务调度程序,删除阻塞.Result(这将返回任务)和Unwrap嵌套任务:

return
    await
        httpClient.PostAsJsonAsync(action, parameters, ctk).ContinueWith(
            x => x.Result.Content.ReadAsAsync<T>(new[] { formatter }), 
            ctk,
            TaskContinuationOptions.None, 
            TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();

那就是说,你真的不需要ContinueWith这里增加的复杂性:

var x = await httpClient.PostAsJsonAsync(action, parameters, ctk);
return await x.Content.ReadAsAsync<T>(new[] { formatter });

Stephen Toub的以下文章具有高度相关性:

"Async Performance: Understanding the Costs of Async and Await"

  

如果我必须在同步上下文中调用异步方法,那么使用await   这是不可能的,最好的方法是什么?

您几乎不需要混合awaitContinueWith,您应该坚持使用await。基本上,如果您使用async,则必须是异步"all the way"

对于服务器端ASP.NET MVC / Web API执行环境,它只是意味着控制器方法应为async并返回TaskTask<>,请检查{{3 }}。 ASP.NET会跟踪给定HTTP请求的待处理任务。在完成所有任务之前,请求尚未完成。

如果确实需要从ASP.NET中的同步方法调用async方法,则可以使用AsyncManager之类的task.Wait()来注册待定任务。对于经典ASP.NET,您可以使用this

在最坏的情况下,您将调用async并阻止,否则您的任务可能会继续超出该特定HTTP请求的边界。

对于客户端UI应用程序,可以使用同步方法调用ContinueWith(action, TaskScheduler.FromCurrentSynchronizationContext())方法的一些不同方案。例如,您可以使用action并从{{1}}触发完成事件(例如this)。

答案 1 :(得分:3)

async和await不应该创建大量线程,特别是不能只有16个用户。实际上,它应该可以帮助您更好地利用线程。在MVC中async和await的目的是在忙于处理IO绑定任务时实际放弃线程池线程。这告诉我你在某个地方做了一些愚蠢的事情,比如产生线程然后无限期地等待。

仍然,900线程并不是真的很多,如果他们使用100%cpu,那么他们就不会等待......他们正在咀嚼什么。这是你应该研究的东西。你说过你使用过像NewRelic这样的工具,他们指出这个CPU使用的来源是什么?有什么方法?

如果我是你,我首先会证明仅使用异步和等待不是导致问题的原因。只需创建一个模仿行为的简单站点,然后对其运行相同的测试。

其次,获取应用程序的副本,然后开始剥离内容然后针对它运行测试。看看你是否可以追踪问题的确切位置。

答案 2 :(得分:3)

有很多东西需要讨论。

首先,当您的应用程序几乎没有业务逻辑时,async / await可以自然地帮助您。我的意思是async / await的意思是睡眠模式中没有很多线程在等待某些东西,主要是一些IO,例如数据库查询(和获取)。如果你的应用程序使用cpu 100%执行巨大的业务逻辑,async / await对你没有帮助。

900线程的问题是它们效率低下 - 如果它们并发运行。关键是,当服务器具有核心/处理器时,拥有这么多“业务”线程会更好。原因是线程上下文切换,锁争用等。有许多系统,如LMAX distruptor模式或Redis,它们在一个线程(或每个核心一个线程)中处理数据。它更好,因为你不必处理锁定。

如何达到描述的方法?查看disruptor,对传入的请求进行排队并逐个处理它们而不是并行处理。

相反的方法,当几乎没有业务逻辑,并且许多线程只是等待IO是将async / await放入工作的好地方。

它是如何工作的:有一个从网络读取字节的线程 - 大多只有一个。一旦某些请求到达,该线程将读取数据。处理请求的工作线程池也很有限。异步的要点是,一旦一个处理线程正在等待某些事情,主要是io,db,该线程将以轮询方式返回,并可用于另一个请求。一旦IO响应准备就绪,就会使用池中的某些线程来完成处理。这就是你如何在一秒钟内使用少量线程来服务千位请求的方式。

我建议您应该绘制一些图片,了解您的网站是如何工作的,每个线程做什么以及它是如何同时工作的。请注意,有必要确定吞吐量或延迟对您来说是否重要。