【问题标题】:How to dispose asynchronously?如何异步处理?
【发布时间】:2008-12-30 12:14:26
【问题描述】:

假设我有一个实现 IDisposable 接口的类。像这样的:

MyClass 使用一些非托管资源,因此来自 IDisposableDispose() 方法会释放这些资源。 MyClass 应该这样使用:

using ( MyClass myClass = new MyClass() ) {
    myClass.DoSomething();
}

现在,我想实现一个异步调用 DoSomething() 的方法。我向 MyClass 添加了一个新方法:

现在,从客户端来看,MyClass 应该像这样使用:

using ( MyClass myClass = new MyClass() ) {
    myClass.AsyncDoSomething();
}

但是,如果我不做任何其他事情,这可能会失败,因为对象 myClass 可能会在调用 DoSomething() 之前被释放(并抛出意外的 ObjectDisposedException)。因此,对 Dispose() 方法的调用(隐式或显式)应该延迟到对 DoSomething() 的异步调用完成。

我认为Dispose() 方法中的代码应该以异步方式执行,并且只有在所有异步调用都解决后。我想知道实现这一点的最佳方法。

谢谢。

注意:为了简单起见,我没有详细介绍 Dispose() 方法是如何实现的。在现实生活中我通常关注Dispose pattern


更新:非常感谢您的回复。我感谢你的努力。作为chakrithas commented,我需要可以多次调用异步DoSomething。理想情况下,这样的事情应该可以正常工作:

using ( MyClass myClass = new MyClass() ) {

    myClass.AsyncDoSomething();
    myClass.AsyncDoSomething();

}

我会研究计数信号量,这似乎是我在寻找的东西。也可能是设计问题。如果我觉得方便的话,我将与您分享一些真实案例以及 MyClass 的真正作用。

【问题讨论】:

    标签: c# asynchronous dispose


    【解决方案1】:

    看起来您正在使用基于事件的异步模式 (see here for more info about .NET async patterns),因此您通常会在类上发生一个事件,该事件在异步操作完成时触发,名为 DoSomethingCompleted(请注意 @ 987654323@ 真的应该被称为DoSomethingAsync 以正确地遵循模式)。暴露此事件后,您可以编写:

    var myClass = new MyClass();
    myClass.DoSomethingCompleted += (sender, e) => myClass.Dispose();
    myClass.DoSomethingAsync();
    

    另一种选择是使用IAsyncResult 模式,您可以将调用dispose 方法的委托传递给AsyncCallback 参数(有关此模式的更多信息也在上面的页面中)。在这种情况下,您将使用 BeginDoSomethingEndDoSomething 方法而不是 DoSomethingAsync,并将其称为...

    var myClass = new MyClass();
    myClass.BeginDoSomething(
        asyncResult => {
                           using (myClass)
                           {
                               myClass.EndDoSomething(asyncResult);
                           }
                       },
        null);        
    

    但无论您采用哪种方式,您都需要一种方法来通知调用方异步操作已完成,以便它可以在正确的时间处理对象。

    【讨论】:

    • 我对在异步回调中处理对象的一个​​主要担忧是,我不知道任何普遍可行的方法来知道何时安全这样做。即使没有其他人正在使用调用Dispose 的对象,其他人也可能正在使用Dispose 方法会影响的对象。
    【解决方案2】:

    异步方法通常有一个回调,允许您在完成时执行一些操作。如果这是你的情况,它会是这样的:

    // The async method taks an on-completed callback delegate
    myClass.AsyncDoSomething(delegate { myClass.Dispose(); });
    

    另一种解决方法是异步包装器:

    ThreadPool.QueueUserWorkItem(delegate
    {
        using(myClass)
        {
            // The class doesn't know about async operations, a helper method does that
            myClass.DoSomething();
        }
    });
    

    【讨论】:

      【解决方案3】:

      我认为不幸的是,Microsoft 没有要求作为 IDisposable 合同的一部分,实现应允许从任何线程上下文调用 Dispose,因为创建对象无法强制继续创建它的线程上下文的存在。可以设计代码,以便创建对象的线程以某种方式监视对象变得过时,并且可以在方便时Dispose,这样当线程不再需要其他任何东西时,它会一直存在直到一切合适对象一直是Disposed,但我认为没有一个标准机制不需要创建Dispose的线程部分的特殊行为。

      您最好的选择可能是在一个公共线程(可能是 UI 线程)中创建所有感兴趣的对象,尽量保证线程将在感兴趣的对象的生命周期内一直存在,并使用类似 @ 987654326@请求处理对象。前提是对象创建和清理都不会阻塞任何时间,这可能是一个好方法,但如果任何一个操作都可以阻塞不同的方法,则可能需要[也许用自己的线程打开一个隐藏的虚拟表单,所以可以使用Control.BeginInvoke那里]。

      或者,如果您可以控制 IDisposable 实现,请将它们设计为可以安全地异步触发它们。在许多情况下,只要没有人在处理该物品时尝试使用它,这将“正常工作”,但这几乎不是给定的。特别是,对于许多类型的IDisposable,存在多个对象实例可能同时操纵公共外部资源的真正危险[例如。一个对象可以保存一个List<> 的已创建实例,在构造实例时将实例添加到该列表中,并删除Dispose 上的实例;如果列表操作未同步,则异步 Dispose 可能会损坏列表,即使 正在处理的对象 没有在其他情况下使用。

      顺便说一句,一个有用的模式是让对象在使用时允许异步处理,期望这种处理会导致任何正在进行的操作在第一个方便的机会抛出异常。像套接字这样的东西就是这样工作的。可能无法提前退出读取操作而不使其套接字处于无用状态,但如果套接字永远不会被使用,那么如果另一个线程已经确定,那么读取就没有必要继续等待数据了它应该放弃。恕我直言,这就是所有 IDisposable 对象应该努力表现的方式,但我知道没有文档需要这种通用模式。

      【讨论】:

        【解决方案4】:

        从 C#8.0 开始,您可以使用IAsyncDisposable

        using System.Threading.Tasks;
        
        public class ExampleAsyncDisposable : IAsyncDisposable
        {
            public async ValueTask DisposeAsync()
            {
                // await DisposeAllTheThingsAsync();
            }
        }
        

        Here 是对微软官方文档的引用。

        【讨论】:

          【解决方案5】:

          我不会以某种方式更改代码以允许异步处理。相反,我会确保在调用 AsyncDoSomething 时,它将拥有它需要执行的所有数据的副本。该方法应负责清理其所有资源。

          【讨论】:

            【解决方案6】:

            您可以添加回调机制并将清理函数作为回调传递。

            var x = new MyClass();
            
            Action cleanup = () => x.Dispose();
            
            x.DoSomethingAsync(/*and then*/cleanup);
            

            但是如果你想对同一个对象实例运行多个异步调用,这会带来问题。

            一种方法是使用Semaphore class 实现一个简单的counting semaphore 来计算正在运行的异步作业的数量。

            将计数器添加到 MyClass 并在每次 AsyncWhatever 调用时递增计数器,在退出时递减它。当信号量为 0 时,类就可以被释放了。

            var x = new MyClass();
            
            x.DoSomethingAsync();
            x.DoSomethingAsync2();
            
            while (x.RunningJobsCount > 0)
                Thread.CurrentThread.Sleep(500);
            
            x.Dispose();
            

            但我怀疑这是否是理想的方式。我闻到了设计问题。也许重新考虑 MyClass 设计可以避免这种情况?

            你能分享一些 MyClass 的实现吗?它应该做什么?

            【讨论】:

            • 循环睡眠被认为是不好的做法。您应该使用 EventWaitHandle 在完成后触发事件。
            • 不幸的是,我仅限于 .NET CF 1.0,并且仅从 .NET 2.0 开始支持 Semaphore 类。
            【解决方案7】:

            这是对这个老问题的更现代的解释。

            真正的目标是跟踪异步任务并等待它们完成......

            public class MyExample : IDisposable
            {
                private List<Task> tasks = new List<Task>();
            
                public async Task DoSomething()
                {
                    // Track your async Tasks
                    tasks.Add(DoSomethingElseAsync());
                    tasks.Add(DoSomethingElseAsync());
                    tasks.Add(DoSomethingElseAsync());
                }
            
                public async Task DoSomethingElseAsync()
                {
                    // TODO: something else
                }
            
                public void Dispose()
                {
                    // Block until Tasks finish
                    Task.WhenAll(tasks);
            
                    // NOTE: C# allows DisposeAsync()
                    // Use non-blocking "await Task.WhenAll(tasks)"
                }
            }
            

            考虑将其转换为可重用的基类。

            有时我对静态方法使用类似的模式...

            public static async Task MyMethod()
            {
                List<Task> tasks = new List<Task>();
            
                // Track your async Tasks
                tasks.Add(DoSomethingElseAsync());
                tasks.Add(DoSomethingElseAsync());
                tasks.Add(DoSomethingElseAsync());
            
                // Wait for Tasks to complete
                await Task.WhenAll(tasks);
            }
            

            【讨论】:

              【解决方案8】:

              所以,我的想法是保持有多少 AsyncDoSomething() 等待完成,并且仅在此计数达到零时才处理。我最初的做法是:

              public class MyClass : IDisposable {
              
                  private delegate void AsyncDoSomethingCaller();
                  private delegate void AsyncDoDisposeCaller();
              
                  private int pendingTasks = 0;
              
                  public DoSomething() {
                      // Do whatever.
                  }
              
                  public AsyncDoSomething() {
                      pendingTasks++;
                      AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller();
                      caller.BeginInvoke( new AsyncCallback( EndDoSomethingCallback ), caller);
                  }
              
                  public Dispose() {
                      AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller();
                      caller.BeginInvoke( new AsyncCallback( EndDoDisposeCallback ), caller);
                  }
              
                  private DoDispose() {
                      WaitForPendingTasks();
              
                      // Finally, dispose whatever managed and unmanaged resources.
                  }
              
                  private void WaitForPendingTasks() {
                      while ( true ) {
                          // Check if there is a pending task.
                          if ( pendingTasks == 0 ) {
                              return;
                          }
              
                          // Allow other threads to execute.
                          Thread.Sleep( 0 );
                      }
                  }
              
                  private void EndDoSomethingCallback( IAsyncResult ar ) {
                      AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState;
                      caller.EndInvoke( ar );
                      pendingTasks--;
                  }
              
                  private void EndDoDisposeCallback( IAsyncResult ar ) {
                      AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState;
                      caller.EndInvoke( ar );
                  }
              }
              

              如果两个或多个线程尝试同时读取/写入 pendingTasks 变量,可能会出现一些问题,因此应使用 lock 关键字来防止竞争条件:

              public class MyClass : IDisposable {
              
                  private delegate void AsyncDoSomethingCaller();
                  private delegate void AsyncDoDisposeCaller();
              
                  private int pendingTasks = 0;
                  private readonly object lockObj = new object();
              
                  public DoSomething() {
                      // Do whatever.
                  }
              
                  public AsyncDoSomething() {
                      lock ( lockObj ) {
                          pendingTasks++;
                          AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller();
                          caller.BeginInvoke( new AsyncCallback( EndDoSomethingCallback ), caller);
                      }
                  }
              
                  public Dispose() {
                      AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller();
                      caller.BeginInvoke( new AsyncCallback( EndDoDisposeCallback ), caller);
                  }
              
                  private DoDispose() {
                      WaitForPendingTasks();
              
                      // Finally, dispose whatever managed and unmanaged resources.
                  }
              
                  private void WaitForPendingTasks() {
                      while ( true ) {
                          // Check if there is a pending task.
                          lock ( lockObj ) {
                              if ( pendingTasks == 0 ) {
                                  return;
                              }
                          }
              
                          // Allow other threads to execute.
                          Thread.Sleep( 0 );
                      }
                  }
              
                  private void EndDoSomethingCallback( IAsyncResult ar ) {
                      lock ( lockObj ) {
                          AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState;
                          caller.EndInvoke( ar );
                          pendingTasks--;
                      }
                  }
              
                  private void EndDoDisposeCallback( IAsyncResult ar ) {
                      AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState;
                      caller.EndInvoke( ar );
                  }
              }
              

              我发现这种方法存在问题。由于资源的释放是异步完成的,这样的事情可能会起作用:

              MyClass myClass;
              
              using ( myClass = new MyClass() ) {
                  myClass.AsyncDoSomething();
              }
              
              myClass.DoSomething();
              

              当在 using 子句之外调用 DoSomething() 时,预期的行为应该是启动 ObjectDisposedException。但我认为这还不足以重新考虑这个解决方案。

              【讨论】:

              • 在 using (MyClass myclass= new....) 示例中,我认为 myclass 对象在 using 块的末尾变得未锚定并且可以完成。我很确定你需要在 using 块结束之前调用并阻止 Waiting 方法是安全的。
              【解决方案9】:

              我不得不去老学校。不,您不能使用简化的“使用”块。但是 Using 块只是用于清理半复杂的 try/catch/finally 块的语法糖。像使用任何其他方法一样构建您的 dispose,然后在 finally 块中调用它。

                  public async Task<string> DoSomeStuffAsync()
                  {
                      // used to be a simple:
                      //    using(var client = new SomeClientObject())
                      //    {
                      //       string response = await client.OtherAsyncMethod();
                      //       return response;
                      //    }
                      //
                      // Since I can't use a USING block here, we have to go old-school
                      // to catch the async disposable.
                      var client = new SomeClientObject();
                      try
                      {
                          string response = await client.OtherAsyncMethod();
                          return response;
                      }
                      finally
                      {
                          await client.DisposeAsync();
                      }
                  }
              

              它很丑,但非常有效,而且比我见过的许多其他建议简单得多。

              【讨论】:

                猜你喜欢
                • 2018-12-10
                • 1970-01-01
                • 1970-01-01
                • 2019-08-29
                • 2017-06-10
                • 2017-11-20
                • 2019-11-14
                • 2019-05-25
                • 2018-10-03
                相关资源
                最近更新 更多