异步/等待指导:内在特征增殖

时间:2014-10-15 05:48:34

标签: c# .net asynchronous async-await

考虑以下类负责使用.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();
    }
}

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

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

  • 存储库 - 负责我调用我的服务代理,读取结果并返回某种形式的域对象。
  • 控制器 - 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一直很常见......在你看到的async / await主题的大多数示例.NET代码中很常见。

然而,在大多数异步/等待&#34;最佳实践&#34;你看到的文章/视频强调async / await应用于真正的异步操作。

我认为存储库或控制器都不会做任何真正异步的事情 - 这都是在服务代理中完成的。

我是否应该阻止async / await在我的代码中变得如此多产?

以下内容是否更能代表&#34;最佳实践&#34;异步/等待?

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();
    }
}

我离开基地了吗?如果是这样的话,请指出我正确的方向。

2 个答案:

答案 0 :(得分:7)

我认为这些方法异步的 - 但只能凭借API部分。那种异步只会向上传播。

Repository.GetItem()的第二次实施是有问题的,IMO:

  • 在等待人员完成之前,您应该调用awaiter的GetResult()方法。事实上,在我看来,在你自己的代码中直接使用awaiter是唯一有意义的时候就是你实现了自己的等待者。 (这应该是非常罕见的。)在这种情况下,我认为它与使用Task<T>.Result的做法相同,但是任务等待可能验证了在打电话之前,任务已经完成(或出现故障)。
  • 您应该对API响应使用阻塞调用,以便将异步API转换为同步API,而无需特别小心。根据代码的确切性质,它很容易导致死锁......如果在单线程同步上下文中进行阻塞调用,并且异步操作需要返回到同一个同步上下文,那么你就可以了。 ll最终会看到等待任务完成的代码,以及等待线程可用的任务。

这表明您的方法应该具有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)表示您没有&#39} ; t实际上需要该方法返回相同的上下文。

答案 1 :(得分:2)

  

我是否应该阻止async / await在我的代码中变得如此多产?

没有。我认为你最初的尝试是在正确的轨道上。如果您调用异步方法并且需要对结果执行某些操作,那么使用await是最好的方法。 async\await倾向于像这样传播调用堆栈。

但是async\await has a cost,因为每个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

  

然而,在大多数异步/等待&#34;最佳实践&#34;你看到的文章/视频强调async / await应该仅用于真正的异步操作。

我认为通常这个建议是为了阻止开发人员创建名称以Async结尾的异步方法,只需在其中调用Task.RunTask.Factory.StartNew。这是欺骗性的,因为它可以启动一个新线程,而真正的异步操作不会启动一个新线程。这就是使异步变得如此理想的原因,你可以用更少的线程来处理更多的异步操作。

  

以下内容是否更能代表&#34;最佳实践&#34; async / await?

没有。调用apiResponse.GetAwaiter().GetResult();将阻塞调用线程,直到结果可用,并且它可以在GUI线程上导致deadlock