Async/Await指南:内在特征码扩散

本文关键字:特征 Await 指南 Async | 更新日期: 2023-09-27 18:26:30

考虑以下负责使用.NET HttpClient与API通信的类。

public class ApiServiceAgent
{
    public async Task<string> GetItem()
    {
        var client = new HttpClient();
        var uri = new Uri("http://stackoverflow.com");
        var response = await client.GetAsync(uri);
        return await response.Content.ReadAsStringAsync();
    }
}

虽然简化了,但它代表了我如何编写与HTTPAPI通信的代码。

现在考虑一下我希望在我自己的Web API项目中使用这个类。为了简单起见,假设我有两个额外的类(直接或以其他方式)调用这个类。

  • Repository—负责调用服务代理,读取结果并返回某种形式的Domain对象
  • 控制器-ASP.NET Web API控制器-API的入口点,负责将数据返回给调用方API用户

这两个类可能看起来是这样的:

public class Controller : ApiController
{
    public async Task<string> Get()
    {
        var repository = new Repository();
        return await repository.GetItem();
    }
}
public class Repository
{
    public async Task<string> GetItem()
    {
        var serviceAgent = new ApiServiceAgent();
        var apiResponse = await serviceAgent.GetItem();
        //For simplicity/brevity my domain object is a lowercase string
        return apiResponse.ToLower();
    }
}

在上面的两个类中,async/Task方法签名从本质上一直传播到Controller。。。您在大多数示例.NET代码中看到的关于async/await主题的非常常见的内容。

然而,在您目前看到的大多数async/await"最佳实践"文章/视频中,都强调async/await只应用于真正的异步操作。

我认为Repository和Controller都不做任何真正异步的事情——这都是在Service Agent中完成的。

我应该防止async/await在我的代码中变得如此多产吗?

以下内容是否更能代表"最佳实践"async/await?

public class Controller : ApiController
{
    public string Get()
    {
        var repository = new Repository();
        return repository.GetItem();
    }
}
public class Repository
{
    public string GetItem()
    {
        var serviceAgent = new ApiServiceAgent();
        var apiResponseTask = serviceAgent.GetItem();
        var apiResponse = apiResponse.GetAwaiter().GetResult();
        return apiResponse.ToLower();
    }
}

我离基地很远吗?如果是的话…请给我指一个正确的方向。

Async/Await指南:内在特征码扩散

我认为这些方法是异步的,但仅取决于API部分。这种异步只是向上传播。

你的Repository.GetItem()的第二个实现有问题,IMO:

  • 在awaiter完成之前,您应该而不是调用awaiter的GetResult()方法。事实上,在我看来,直接在自己的代码中使用awaiter唯一有意义的时候是在实现自己的awaiter时。(这种情况应该非常罕见。)在这种情况下,我认为它将与使用Task<T>.Result相同,但可能是任务唤醒器在调用之前验证任务是否已完成(或出现故障)
  • 您应该而不是在API响应上使用阻塞调用,将异步API转换为同步API,而不需要太多注意。根据代码的确切性质,它很容易导致死锁。。。如果阻塞调用是在单个线程同步上下文中进行的,并且异步操作需要返回到同一同步上下文,那么代码将等待任务完成,任务将等待线程可用

顺便说一句,这表明您的方法应该有一个Async后缀,但这并不意味着它们必须是async方法。第二个可能是这样,但第一个可以简单地写为:

public class Controller : ApiController
{
    public Task<string> GetAsync()
    {
        var repository = new Repository();
        return repository.GetItemAsync();
    }
}

如果方法async只有一个直接用于返回值的await,那么它几乎没有什么好处。

对于您的Repository方法,await将非常有用,因为您有代码要在它完成后运行——您可以直接使用ContinueWith,但这会更痛苦。所以我将其保留为async/await版本,只是将其重命名为GetItemAsync。。。并且可能使用ConfigureAwait(false)来指示您实际上不需要该方法来返回到相同的上下文中。

我应该防止async/await在我的代码中变得如此多产吗?

没有。我认为你最初的尝试是正确的。如果您调用和异步方法,并且需要对结果进行处理,那么使用await是最好的方法。CCD_ 16倾向于像那样向上传播调用堆栈。

然而,async'await具有成本,因为为每个async方法生成状态机。如果您不需要异步操作的结果,而只是将其返回给调用者,那么您可以只返回被调用的async方法的结果,以避免生成状态机。

例如:

public class Repository
{
    public Task<string> GetItem()
    {
        var serviceAgent = new ApiServiceAgent();
        // Don't need to do anything with the result of GetItem, just return the task.
        return serviceAgent.GetItem();
        //var apiResponse = await serviceAgent.GetItem();
        //For simplicity/brevity my domain object is a lowercase string
        //return apiResponse.ToLower(); 
    }
}

当然,如果在返回GetItem的结果之前需要对其进行处理,那么方法必须是async

然而,在您目前看到的大多数async/await"最佳实践"文章/视频中,都强调async/await只能用于真正的异步操作。

我认为给出这个建议通常是为了阻止开发人员创建名称以Async结尾的异步方法,而这些方法只是在内部调用Task.RunTask.Factory.StartNew。这是欺骗性的,因为它可以启动新线程,而真正的异步操作不会启动新线程。这就是异步如此可取的原因,您可以用更少的线程处理更多的异步操作。

以下内容是否更能代表"最佳实践"async/await?

没有。调用apiResponse.GetAwaiter().GetResult();将阻塞调用线程,直到结果可用,并且可能导致GUI线程上的死锁。