How to dispose asynchronously?

2019-02-08 00:46发布

问题:

Let's say I have a class that implements the IDisposable interface. Something like this:

MyClass uses some unmanaged resources, hence the Dispose() method from IDisposable releases those resources. MyClass should be used like this:

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

Now, I want to implement a method that calls DoSomething() asynchronously. I add a new method to MyClass:

Now, from the client side, MyClass should be used like this:

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

However, if I don't do anything else, this could fail as the object myClass might be disposed before DoSomething() is called (and throw an unexpected ObjectDisposedException). So, the call to the Dispose() method (either implicit or explicit) should be delayed until the asynchronous call to DoSomething() is done.

I think the code in the Dispose() method should be executed in a asynchronous way, and only once all asynchronous calls are resolved. I'd like to know which could be the best way to accomplish this.

Thanks.

NOTE: For the sake of simplicity, I haven't entered in the details of how Dispose() method is implemented. In real life I usually follow the Dispose pattern.


UPDATE: Thank you so much for your responses. I appreciate your effort. As chakrit has commented, I need that multiple calls to the async DoSomething can be made. Ideally, something like this should work fine:

using ( MyClass myClass = new MyClass() ) {

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

}

I'll study the counting semaphore, it seems what I'm looking for. It could also be a design problem. If I find it convenient, I will share with you some bits of the real case and what MyClass really does.

回答1:

It looks like you're using the event-based async pattern (see here for more info about .NET async patterns) so what you'd typically have is an event on the class that fires when the async operation is completed named DoSomethingCompleted (note that AsyncDoSomething should really be called DoSomethingAsync to follow the pattern correctly). With this event exposed you could write:

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

The other alternative is to use the IAsyncResult pattern, where you can pass a delegate that calls the dispose method to the AsyncCallback parameter (more info on this pattern is in the page above too). In this case you'd have BeginDoSomething and EndDoSomething methods instead of DoSomethingAsync, and would call it something like...

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

But whichever way you do it, you need a way for the caller to be notified that the async operation has completed so it can dispose of the object at the correct time.



回答2:

Async methods usually have a callback allowing you to do do some action upon completition. If this is your case it would be something like this:

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

An other way around this is an async wrapper:

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


回答3:

I wouldn't alter the code somehow to allow for async disposes. Instead I would make sure when the call to AsyncDoSomething is made, it will have a copy of all the data it needs to execute. That method should be responsible for cleaning up all if its resources.



回答4:

I consider it unfortunate that Microsoft didn't require as part of the IDisposable contract that implementations should allow Dispose to be called from any threading context, since there's no sane way the creation of an object can force the continued existence of the threading context in which it was created. It's possible to design code so that the thread which creates an object will somehow watch for the object becoming obsolete and can Dispose at its convenience, and so that when the thread is no longer needed for anything else it will stick around until all appropriate objects have been Disposed, but I don't think there's a standard mechanism that doesn't require special behavior on the part of the thread creating the Dispose.

Your best bet is probably to have all the objects of interest created within a common thread (perhaps the UI thread), try to guarantee that the thread will stay around for the lifetime of the objects of interest, and use something like Control.BeginInvoke to request the objects' disposal. Provided that neither object creation nor cleanup will block for any length of time, that may be a good approach, but if either operation could block a different approach may be needed [perhaps open up a hidden dummy form with its own thread, so one can use Control.BeginInvoke there].

Alternatively, if you have control over the IDisposable implementations, design them so that they can safely be fired asynchronously. In many cases, that will "just work" provided nobody is trying to use the item when it is disposed, but that's hardly a given. In particular, with many types of IDisposable, there's a real danger that multiple object instances might both manipulate a common outside resource [e.g. an object may hold a List<> of created instances, add instances to that list when they are constructed, and remove instances on Dispose; if the list operations are not synchronized, an asynchronous Dispose could corrupt the list even if the object being disposed is not otherwise in use.

BTW, a useful pattern is for objects to allow asynchronous dispose while they are in use, with the expectation that such disposal will cause any operations in progress to throw an exception at the first convenient opportunity. Things like sockets work this way. It may not be possible for a read operation to be exit early without leaving its socket in a useless state, but if the socket's never going to be used anyway, there's no point for the read to keep waiting for data if another thread has determined that it should give up. IMHO, that's how all IDisposable objects should endeavor to behave, but I know of no document calling for such a general pattern.



回答5:

You could add a callback mechanism and pass a cleanup function as a callback.

var x = new MyClass();

Action cleanup = () => x.Dispose();

x.DoSomethingAsync(/*and then*/cleanup);

but this would pose problem if you want to run multiple async calls off the same object instance.

One way would be to implement a simple counting semaphore with the Semaphore class to count the number of running async jobs.

Add the counter to MyClass and on every AsyncWhatever calls increment the counter, on exits decerement it. When the semaphore is 0, then the class is ready to be disposed.

var x = new MyClass();

x.DoSomethingAsync();
x.DoSomethingAsync2();

while (x.RunningJobsCount > 0)
    Thread.CurrentThread.Sleep(500);

x.Dispose();

But I doubt that would be the ideal way. I smell a design problem. Maybe a re-thought of MyClass designs could avoid this?

Could you share some bit of MyClass implementation? What it's supposed to do?



回答6:

So, my idea is to keep how many AsyncDoSomething() are pending to complete, and only dispose when this count reaches to zero. My initial approach is:

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

Some issues may occur if two or more threads try to read / write the pendingTasks variable concurrently, so the lock keyword should be used to prevent race conditions:

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

I see a problem with this approach. As the release of resources is asynchronously done, something like this might work:

MyClass myClass;

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

myClass.DoSomething();

When the expected behavior should be to launch an ObjectDisposedException when DoSomething() is called outside the using clause. But I don't find this bad enough to rethink this solution.