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();
}
}
我离基地很远吗?如果是的话…请给我指一个正确的方向。
我认为这些方法是异步的,但仅取决于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.Run
或Task.Factory.StartNew
。这是欺骗性的,因为它可以启动新线程,而真正的异步操作不会启动新线程。这就是异步如此可取的原因,您可以用更少的线程处理更多的异步操作。
以下内容是否更能代表"最佳实践"async/await?
没有。调用apiResponse.GetAwaiter().GetResult();
将阻塞调用线程,直到结果可用,并且可能导致GUI线程上的死锁。