【问题标题】:How(and why) can I avoid returning a void on these async methods?如何(以及为什么)避免在这些异步方法上返回 void?
【发布时间】:2015-11-30 05:56:26
【问题描述】:

编辑:因此,似乎让方法返回 void 而不是任务意味着异常在错误(意外?)上下文中传播。 但是,我的 IDE (Xamarin) 仍然在我调用 AttemptDatabseLoad() 的构造函数中大惊小怪

"该语句不等待并执行当前方法 在呼叫完成之前继续。考虑使用“等待” 运算符或调用 'Wait' 方法"

为什么要大惊小怪?当然,使用异步方法的全部目的正是为了让程序继续在主线程上执行。

我已经阅读了一些关于 async 和 await 的内容,因为我需要为我正在制作的应用程序加载一些异步数据。我在很多地方读到过让异步方法返回 void 是不好的做法(触发事件的情况除外),并且我理解保持对任务的句柄的原因。 但是,我看不出我在下面写的内容有任何逻辑错误,所以我的问题是双重的:为什么我当前的代码实践不佳?应该怎么改写?

private const int MAX_CONNECTION_ATTEMPTS = 10;
private int ConnectionAttempts = 0;

//Constructor
public DataLoader()
{
    //First load up current data from local sqlite db
    LoadFromLocal();

    //Then go for an async load from 
    AttemptDatabaseLoad();
}

public async void AttemptDatabaseLoad()
{
    while(ConnectionAttempts < MAX_CONNECTION_ATTEMPTS){
        Task<bool> Attempt = TryLoad ();
        bool success = await Attempt;
        if (success) {
            //call func to load data into program memory proper
        }else{
            ConnectionAttempts++;
        }
    }
}

//placeholder for now
public async Task<bool> TryLoad()
{
    await Task.Delay(5000);
    return false;
}

【问题讨论】:

  • 在构造函数中的异步调用没有多大意义——新对象不仅半未初始化,你甚至不知道什么时候它是安全的使用。
  • 我很困惑。当然,每当调用 await 时,执行就会分裂到一个新的上下文中并在主上下文中继续?所以 AttemptDatabaseLoad() 不应该阻塞主(或调用)线程,因为对象构造将和往常一样快。
  • 不,它不会阻塞,那是问题。您将返回一个可能包含或不包含有效数据但您无法知道的对象。您甚至不知道要等多久才能安全使用该对象。 Yuval 的回答和 krim 的回答中的链接解释为避免这种情况

标签: c# asynchronous async-await


【解决方案1】:

您可以将返回类型更改为 Task(非泛型)并且不要从异步方法“显式”返回。可以在此处找到仅在顶级函数上使用 void 更好的原因:async/await - when to return a Task vs void? 因此,它主要是关于从 async-void 方法中的异常中恢复。我希望它会有所帮助。

编辑:还有一件事-因为我没有注意到您是从构造函数中调用它的。请同时查看这个答案:https://stackoverflow.com/a/23051370/580207 和这篇博文:http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html

【讨论】:

    【解决方案2】:

    为什么我当前的代码实践不佳?

    DataLoader() 构造函数的调用者可能会遇到以下问题:

    • 实例化DataLoader 类的代码不知道DataLoader() 返回后加载操作仍在进行中,因此它不能使用异步AttemptDatabaseLoad() 检索到的数据。

    • 无法发现加载的数据何时可用。

    • 它不能组合成更大的异步操作。

    建议的更改是将异步方法返回的任务存储在一个属性中,以便调用者可以使用它来等待加载完成,或者将其组合成一个异步方法。

    class DataLoader
    {
    
    
    public DataLoader ()
    {
        //First load up current data from local sqlite db
        LoadFromLocal();
    
        //Then go for an async load from 
        this.Completion = AttemptDatabaseLoadAsync();
    }
    
    async Task AttemptDatabaseLoadAsync()
    {
        while(ConnectionAttempts < MAX_CONNECTION_ATTEMPTS){
            Task<bool> Attempt = TryLoad ();
            bool success = await Attempt;
            if (success) {
                //call func to load data into program memory proper
            }else{
                ConnectionAttempts++;
            }
        }
    }
    
    public Task Completion
    {
        get; private set;
    }
    
    }
    

    用法:

     var loader = new DataLoader();
     loader.Completion.Wait();
    

    或:

    async Task SomeMethodAsync()
    {
       var loader = new DataLoader();
       await loader.Completion;
    }
    

    【讨论】:

    • 您的第一次使用可能会以死锁结束,因为 Wait() 会阻塞线程。您的第二个用法就是要使用的那个。
    • @GazTheDestroyer:嗯,从 UI 线程调用时确实有可能。将 'ConfigureAwait(false)' 添加到 await Attempt 会缓解死锁问题吗?
    【解决方案3】:

    构造函数的目的是在初始化后将对象带入其完全构造的结构。另一方面,异步方法和构造函数不能很好地配合使用,因为构造函数本质上是同步的。

    解决这个问题的方法通常是公开一个类型的初始化方法,它本身就是异步的。现在,让调用者完全初始化对象。请注意,这将要求您监视方法的实际初始化。

    当您需要扩展时,异步会大放异彩。如果您不希望这成为应用程序中的 IO 瓶颈,也许可以考虑使用同步方法。一旦构造函数完成执行,这将为您提供实际完全初始化对象的好处。虽然,我不认为我会通过构造函数发起对数据库的调用:

    public async Task InitializeAsync()
    {
        LoadFromLocal();
        await AttemptDatabaseLoadAsync();
    }
    
    public async Task AttemptDatabaseLoadAsyncAsync()
    {
        while(ConnectionAttempts < MAX_CONNECTION_ATTEMPTS)
        {
            Task<bool> Attempt = TryLoad ();
            bool success = await Attempt;
            if (success)
            {
                //call func to load data into program memory proper
            }
            else
            {
                ConnectionAttempts++;
            }
        }
    }
    

    并称它为:

    var dataLoader = new DataLoader();
    await dataLoader.InitializeAsync();
    

    【讨论】:

      【解决方案4】:

      我理解为什么最好掌握任务。

      因此,似乎让方法返回 void 而不是任务意味着异常在错误(意外?)上下文中传播。

      拥有Task 的好处之一是您可以使用它来检索异步方法的结果。我所说的“结果”不仅仅指返回值——我也指异常。 Task 表示该异步方法的执行。

      当异常转义async Task 方法时,它会被放置在返回的任务上。当一个异常从async void 方法中逃逸时,它没有明显的地方可以去,所以实际的行为是直接在SynchronizationContext 上引发它,它在async void 方法的开头是当前的。这听起来很奇怪,但它是专门为模拟异常转义事件处理程序而设计的。

      当然,如果您的 async void 方法不是事件处理程序(如本例),那么行为似乎非常奇怪和令人惊讶。

      为什么要大惊小怪?当然,使用异步方法的全部目的正是为了让程序继续在主线程上执行。

      我认为您误解了警告信息。由于Task 代表该方法的执行,因此在 99.9% 的情况下忽略它是错误的。通过忽略它,您的代码表示它不关心异步方法何时完成,它的 return 值是什么(如果有)以及它是否抛出例外情况。代码很少关心任何这些。

      应该如何重写?

      我在how to do "async constructors" 上有一篇博文。我最喜欢的方法是异步工厂方法:

      //Constructor
      private DataLoader()
      {
        //First load up current data from local sqlite db
        LoadFromLocal();
      }
      
      public static async Task<DataLoader> CreateAsync()
      {
        var result = new DataLoader();
        await result.AttemptDatabaseLoadAsync();
        return result;
      }
      

      但是,由于您在 UI 应用程序中使用它,我怀疑您最终会遇到想要从 ViewModel 构造函数调用异步代码的情况。异步工厂非常适合辅助代码(如 DataLoader),但它们不适用于 ViewModel,因为需要立即创建 VM - UI 需要现在显示一些内容 em>。

      在 UI 层,您必须首先将您的 UI 初始化为某种“正在加载”状态,然后在数据到达后将其 更新为“正常”状态。我更喜欢为此使用asynchronous data binding,如我的 MSDN 文章中所述。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-01-03
        • 1970-01-01
        • 2016-08-30
        • 2014-09-04
        • 1970-01-01
        • 1970-01-01
        • 2012-08-08
        相关资源
        最近更新 更多