通过ASP.NET Web API有效地使用异步/等待


114

我正在尝试async/await在我的Web API项目中使用ASP.NET 的功能。我不太确定这是否会对我的Web API服务的性能产生任何影响。请从我的应用程序下面找到工作流程和示例代码。

工作流程:

UI应用程序→Web API端点(控制器)→Web API服务层中的调用方法→调用另一个外部Web服务。(这里有数据库交互等)

控制器:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

服务层:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

我测试了上面的代码并且正在工作。但我不确定这是否是的正确用法async/await。请分享您的想法。


Answers:


202

我不太确定这是否会对我的API性能产生任何影响。

请记住,服务器端异步代码的主要好处是可伸缩性。它不会神奇地使您的请求运行得更快。我ASP.NET的文章中介绍了几个“我应该使用async”的注意事项。async

我认为您的用例(调用其他API)非常适合异步代码,请记住“异步”并不意味着“更快”。最好的方法是首先使您的UI响应和异步;即使稍微慢一点,也可以让您的应用感觉更快。

就代码而言,这不是异步的:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

您需要一个真正的异步实现来获得以下方面的可伸缩性async

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

或(如果您在此方法中的逻辑确实只是传递):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

请注意,像这样从“由内而外”工作比从“由内而外”工作更容易。换句话说,不要以异步控制器动作开始,然后强制下游方法是异步的。相反,请确定自然的异步操作(调用外部API,数据库查询等),并首先使最低级别的异步操作Service.ProcessAsync。然后async放慢速度,最后一步使控制器动作异步。

而且在任何情况下都不应使用Task.Run此方案。


4
感谢Stephen的宝贵意见。我确实将所有服务层方法都更改为异步,现在我使用ExecuteTaskAsync方法调用了外部REST调用,并且按预期方式工作。也感谢您关于异步和任务的博客文章。初步了解对我的帮助很大。
arp

5
您能否解释为什么不使用Task.Run在这里?
马滕

1
@Maarten:我在链接到的文章中对此做了简要说明。我在博客上有更详细的介绍。
Stephen Cleary

我已经读过您的文章Stephen,并且似乎避免提及任何内容。当ASP.NET请求到达并开始在线程池线程上运行时,这很好。但是,如果它变为异步,则是,它将启动异步处理,并且该线程立即返回到池。但是在异步方法中执行的工作本身将在线程池线程上执行!


12

这是正确的,但可能没有用。

由于没有什么可等待的,没有对可以异步运行的阻塞API的调用,因此您正在设置结构来跟踪异步操作(这会产生开销),但是却没有利用该功能。

例如,如果服务层正在使用支持异步调用的Entity Framework执行数据库操作:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

您将允许工作线程在查询数据库时做其他事情(从而能够处理另一个请求)。

Await往往需要一直走下去:很难改装到现有系统中。


-1

您没有有效利用异步/等待,因为执行同步方法时请求线程将被阻塞ReturnAllCountries()

分配给处理请求的线程将在ReturnAllCountries()工作时处于空闲状态。

如果您可以实现ReturnAllCountries()异步,那么您将看到可伸缩性优势。这是因为线程可以在执行时释放回.NET线程池以处理另一个请求ReturnAllCountries()。通过更有效地利用线程,这将使您的服务具有更高的吞吐量。


这不是真的。套接字已连接,但是处理请求的线程可以在等待服务调用的同时执行其他操作,例如处理其他请求。
加尔·戈弗雷

-8

我将您的服务层更改为:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

有了它,您仍在_service.Process同步运行呼叫,等待它并没有带来任何好处。

使用这种方法,您可以将可能很慢的调用包装在中Task,将其启动,然后将其返回以等待。现在,您将获得等待的好处Task


3
根据博客链接,我可以看到,即使我们使用Task.Run方法仍然可以同步运行?
2015年

嗯 上面的代码和该链接之间有很多区别。您Service不是异步方法,因此不会在Service调用本身中等待您。我能理解他的观点,但我认为这并不适用。
Jonesopolis

8
Task.Run应该避免在ASP.NET上使用。这种方法将消除async/的所有好处,await并且实际上在负载下的性能要比同步调用差。
Stephen Cleary
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.