当使用异步web服务时,应该“等待”什么?什么是不应该做的?

本文关键字:什么 等待 不应该 应该 异步 web 服务 | 更新日期: 2023-09-27 18:04:20

我正在开发一个MVC web应用程序,它允许我通过web服务异步管理我的数据。

我的理解是,这允许访问该网站运行的服务器的应用程序池的CPU线程在发出请求后返回到应用程序池,以便它们可以用于服务其他请求而不会使整个线程停滞。

假设我的理解是正确的(尽管它可能是糟糕的措辞),我开始思考什么时候我应该await的事情。考虑下面的函数:

public async Task<ActionResult> Index()
{
    List<User> users = new List<User>();
    using (var client = new HttpClient())
    {
        client.BaseAddress = new Uri("http://localhost:41979");
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(
                       new MediaTypeWithQualityHeaderValue("application/json"));
        HttpResponseMessage response = await client.GetAsync("api/user/");
        if (response.IsSuccessStatusCode)
        {
            users = await response.Content.ReadAsAsync<List<User>>();
        }
    }
    return View(users);
}

我所有的函数看起来都很相似,除了它们用web服务返回的数据做不同的事情,我想知道,我是否也应该等待返回?

类似:

return await View(users);

await return View(users);

我的意思是,到目前为止,网站运行得很好,除了我对web服务应该发送回客户端网站的确切内容有点困惑,但由于我是开发web服务的新手,我仍然想知道我是否在正确地做事情,这已经在我身上吃了一段时间。

当使用异步web服务时,应该“等待”什么?什么是不应该做的?

您只能等待通过Task, Task<T>或自定义awaiter公开异步操作的命名或匿名方法和lambda表达式。由于View本身不做任何异步操作,因此您不能等待它。

使用await的实际意义是什么?通常,你有一些IO绑定操作,本质上是异步的。通过使用异步API,您可以通过返回线程池来允许线程不被阻塞,并利用它来服务不同的请求。async不改变HTTP的本质。它仍然是"请求-响应"。当async方法产生控制时,它不会向客户端返回响应。它只会在操作完成后返回。

View(object obj)返回一个ViewResult,它将把您的对象转换为所需的输出。ViewResult不是可等待的,因为它没有通过可等待对象公开任何"承诺"。因此,您不能异步地等待它。

我开始考虑什么时候该等东西

最好总是等待异步调用的结果。

如果你不等待,你火了&忘记,在成功和错误情况下,您都不会在您的端收到响应。

当您需要将异步任务作为值展开时,您可以"等待"。否则,您可以返回一个任务,并在需要时稍后运行它。还要注意. wait()与await不同。Wait()将阻塞直到任务完成(旁注我知道)。还要检查你的代码,你的方法签名中有语法错误:

public async <Task>ActionResult> Index()
应该

public async Task<ActionResult> Index()

我认为这是一个非常好的问题,但也很难回答,尤其是在一个网站的情况下。

我对web服务应该发送回客户端网站的内容有点困惑

最重要的是要理解如果你使用async/await,那么你的动作方法代码仍然是序列化的,我的意思是下一行将只在async操作完成后执行。但是会有(至少)三个线程:

  • action方法所在的原始web服务器工作线程调用。最初MVC基础架构从池,并将此线程专用于服务当前请求。
  • 可选的线程是由异步操作启动的用await调用。
  • 一个延续线程(也是来自池)中,您的操作方法在等待之后继续。请注意这个线程将具有相同的上下文(HttpContext,用户文化)原来的worker是什么,所以对你来说是透明的,但它将是从池中新分配的另一个线程。

乍一看,它没有任何意义:如果action方法中的操作仍然是序列化的,为什么所有这些线程都被占用?我花了同样的时间……要理解这一点,我们必须看一下桌面应用程序,比如WPF应用程序。

简而言之:有一个消息循环和一个专用线程,它从队列中读取UI(和其他虚拟)事件,并在该线程的上下文中执行事件处理程序,如按钮单击事件。如果你阻塞这个线程2秒,UI将在这段时间内无响应。因此,这就是为什么我们希望在另一个线程中以异步方式做许多事情,并让专用(UI)线程快速返回并处理队列中的其他消息。然而,这可能非常不方便,因为有时我们希望在异步操作结束后继续一些东西,并且我们已经得到了结果。c#的async/await特性使得这非常方便。在这种情况下,延续线程始终是专用的UI线程,但在其无限循环的(非常)后一轮。总结一下:

在事件处理程序中,你可以使用async/await来执行你的序列化操作,延续将始终在原始的专用UI线程中完成,但在等待期间,该线程可以循环,并处理其他消息。

现在回到MVC操作方法:在这种情况下,您的操作方法类似于事件处理程序。尽管仍然很难理解线程复杂性的使用:在这种情况下,阻塞action方法不会阻塞任何东西(因为它阻塞了WPF中的专用UI线程)。以下是事实:

  • 其他客户端(浏览器)请求将由其他线程处理。(原来如此不阻塞任何阻塞这个线程(*)继续阅读
  • 这个请求不会被更快地服务,即使我们使用async/await,因为操作被序列化,我们必须等待(await)的结果异步操作。(不要混淆async/await和并行处理)

所以async/await和透明线程技巧的使用并不像在桌面应用程序中那么明显。

Still: Pool不是一个无穷无尽的资源。可能可以更好地利用线程池,可能可以在极端压力下可以使web服务器的性能更好。