为什么我应该创建异步的WebAPI操作而不是同步的操作?

112

我在创建的Web API中有以下操作:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public CartTotalsDTO GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh);
}

通过Jquery Ajax调用进行对此Web服务的调用,方式如下:

$.ajax({
      url: "/api/products/pharmacies/<%# Farmacia.PrimaryKeyId.Value.ToString() %>/page/" + vm.currentPage() + "/" + filter,
      type: "GET",
      dataType: "json",
      success: function (result) {
          vm.items([]);
          var data = result.Products;
          vm.totalUnits(result.TotalUnits);
      }          
  });

我见过一些开发者是这样实现上一个操作的:

// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
    return await Task.Factory.StartNew(() => delegateHelper.GetProductsWithHistory(CustomerContext.Current.GetContactById(pharmacyId), refresh));
}

不过必须得说,GetProductsWithHistory() 是一个相当耗时的操作。鉴于我的问题和背景,将 webAPI 操作转为异步操作对我有什么好处呢?


1
客户端使用 AJAX,它已经是异步的了。你不需要将服务也写成 async Task<T>。记住,AJAX 是在 TPL 甚至不存在之前实现的 :) - Dominic Zukiewicz
67
你需要理解为什么要使用异步控制器,许多人不明白。IIS可用的线程数量有限,当所有线程都在使用时,服务器无法处理新请求。通过使用异步控制器,当一个进程正在等待I/O完成时,它的线程就会被释放出来,以便服务器可以用于处理其他请求。 - Matija Grcic
3
你见过哪些开发者这样做?如果有任何推荐这种技术的博客文章或文章,请提供链接。 - Stephen Cleary
3
只有当整个流程都是异步感知的(包括Web应用程序本身和控制器),从顶部到任何可以等待的活动,如定时器延迟、文件I/O、数据库访问和Web请求,才能充分发挥异步的优势。在这种情况下,您的委托助手需要一个返回Task<CartTotalsDTO>的“GetProductsWithHistoryAsync()”。如果您打算将其调用迁移到异步方式,那么编写异步控制器也可能会带来好处;然后在迁移剩余部分时开始从异步部分受益。 - Keith Robertson
1
如果你正在进行的进程会去访问数据库,那么你的网络线程就只是在等待它返回并保持该线程。如果你已经达到了最大线程数并且另一个请求进来了,那么它必须等待。为什么要这样做呢?相反,你应该释放控制器中的该线程,以便另一个请求可以使用它,并且只有在来自数据库的原始请求返回时才占用另一个网络线程。https://msdn.microsoft.com/zh-cn/magazine/dn802603.aspx - user441521
显示剩余2条评论
2个回答

101
在您的具体示例中,该操作根本不是异步的,所以您正在进行异步超同步操作。您只是释放一个线程并阻止另一个线程。这没有任何理由,因为所有线程都是线程池线程(与GUI应用程序不同)。
在我的“异步超同步”讨论中,我强烈建议,如果您有一个内部以同步方式实现的API,则不应公开仅将同步方法包装在Task.Run中的异步对应项。
来自我应该为同步方法公开异步包装器吗? 但是,在进行WebAPI调用时,如果存在实际的异步操作(通常是I/O),则使用async而不是阻塞一个等待结果的线程,该线程返回到线程池,因此能够执行其他操作。总体而言,这意味着您的应用程序可以使用更少的资源做更多的事情,从而提高可扩展性。

3
@efaruk 所有的线程都是工作线程。释放一个线程池线程并阻塞另一个线程是没有意义的。 - i3arnon
1
@efaruk,我不确定你想说什么...但只要你同意在WebAPI中没有使用异步而是同步的原因,那就没问题了。 - i3arnon
@efaruk,“异步优于同步”(即await Task.Run(() => CPUIntensive()))在asp.net中是无用的。这样做并没有任何好处。你只是释放了一个线程池线程来占用另一个线程。这比简单调用同步方法效率低。 - i3arnon
1
@efaruk 不,那不是合理的。你的例子是按顺序运行独立的任务。在提出建议之前,你真的需要了解一下异步/等待。你需要使用await Task.WhenAll才能并行执行。 - Søren Boisen
1
正如Boisen所解释的那样,您的示例在顺序调用这些同步方法时并没有增加任何价值。如果您想将负载并行化到多个线程上,可以使用Task.Run,但这不是“异步优先于同步”的含义。“异步优先于同步”是指创建一个异步方法作为同步方法的包装器。您可以在我的回答中看到引用。 - i3arnon
显示剩余3条评论

1
一种方法是(我在客户应用中成功使用过)使用Windows服务运行具有工作线程的长时间操作,然后在IIS中执行此操作,以释放线程直到阻塞操作完成: 注意,这假定结果存储在表格中(行由jobId标识),并且清理进程在使用几个小时后清理它们。
回答问题,“考虑到我的问题和上下文,使webAPI操作异步化将如何使我受益?”考虑到这是“相当长的操作”,我认为不是毫秒而是几秒钟,这种方法可以释放IIS线程。显然,您还必须运行一个Windows服务,它本身需要资源,但是这种方法可以防止大量缓慢的查询从其他系统部分窃取线程。
// GET api/<controller>
[HttpGet]
[Route("pharmacies/{pharmacyId}/page/{page}/{filter?}")]
public async Task<CartTotalsDTO> GetProductsWithHistory(Guid pharmacyId, int page, string filter = null ,[FromUri] bool refresh = false)
{
        var jobID = Guid.NewGuid().ToString()
        var job = new Job
        {
            Id = jobId,
            jobType = "GetProductsWithHistory",
            pharmacyId = pharmacyId,
            page = page,
            filter = filter,
            Created = DateTime.UtcNow,
            Started = null,
            Finished = null,
            User =  {{extract user id in the normal way}}
        };
        jobService.CreateJob(job);

        var timeout = 10*60*1000; //10 minutes
        Stopwatch sw = new Stopwatch();
        sw.Start();
        bool responseReceived = false;
        do
        {
            //wait for the windows service to process the job and build the results in the results table
            if (jobService.GetJob(jobId).Finished == null)
            {
                if (sw.ElapsedMilliseconds > timeout ) throw new TimeoutException();
                await Task.Delay(2000);
            }
            else
            {
                responseReceived = true;
            }
        } while (responseReceived == false);

    //this fetches the results from the temporary results table
    return jobService.GetProductsWithHistory(jobId);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接