locked
Dealing with async disposables?

    Question

  • Okay, so I'm (rarely) running up against a use case where I've got two concurrent tasks, each with a disposable result.

    Task<IDisposable> dt1 = Async1();
    Task<IDisposable> dt2 = Async2();
    
    using(IDisposable d1 = await dt1) // if first await throws, d2 is never disposed!
    using(IDisposable d2 = await dt2)
    {
    ...
    }

    I've worked around this by making a small utility class I can use something like this:

    Task<IDisposable> dt1 = Async1();
    Task<IDisposable> dt2 = Async2();
    
    using(IDisposable safety = AsyncDisposer(dt1, dt2))
    {
      IDisposable d1 = await dt1;
      IDisposable d2 = await dt2;
    }

    I'm curious if anyone else has encountered this. If so, what was your solution?

    Wednesday, February 8, 2012 6:23 AM

Answers

  • Fundamentally, any using-based approach for this case is going to be limited by the fact that "using" is tied to IDisposable, and IDisposable.Dispose is synchronous.  If dt1 fails, such that you end up in the IDisposable.Dispose of your AsyncDisposer, there's no guarantee that dt2 has already completed, and thus if you need to wait until dt2 has completed (in order to either dispose of the returned IDisposable or propagate the exception), you'll need to synchronously block waiting for the operation to complete... that may or may not be acceptable to your needs (e.g. maybe exceptions are really rare here, and the extra bit of blocking in this case won't have any noticeable impact).

    If you don't need to do anything with the result of dt1 before awaiting dt2, you could use Task.WhenAll to make this easier, e.g.

    Task<IDisposable> dt1 = Async1();
    Task<IDisposable> dt2 = Async2();
    try
    {
        var disposables = await Task.WhenAll(dt1, dt2);
        using(IDisposable d1 = disposables[0])
        using(IDisposable d2 = disposables[1])
        {
            ... // work with d1 and d2 here
        }
    }
    catch
    {
        if (dt1.Status == TaskStatus.RanToCompletion) dt1.Result.Dispose();
        if (dt2.Status == TaskStatus.RanToCompletion) dt2.Result.Dispose();
        throw;
    }

    And you could encapsulate that into an async helper method which accepted the "work with d1 and d2 here" as a delegate to execute, e.g.

    static async Task RunAndDisposeAsync(
        Task<IDisposable> first, Task<IDisposable> second,
        Func<IDisposable,IDisposable,Task> func)
    {
        try
        {
            var disposables = await Task.WhenAll(first, second);
            using (IDisposable d1 = disposables[0], d2 = disposables[1])
                await func(d1, d2);
        }
        catch
        {
            if (first.Status == TaskStatus.RanToCompletion) first.Result.Dispose();
            if (second.Status == TaskStatus.RanToCompletion) second.Result.Dispose();
            throw;
        }
    }

    which in your example you'd then use like:

    await RunAndDisposeAsync(Async1(), Async2(), async (d1,d2) =>
    {
        ... // work with d1 and d2 here
    }); 

    You could also deal with this completely asynchronously by expanding out your original code to not use "using", though that will of course be some more code.

    Thursday, November 29, 2012 5:32 PM
    Moderator

All replies

  • Fundamentally, any using-based approach for this case is going to be limited by the fact that "using" is tied to IDisposable, and IDisposable.Dispose is synchronous.  If dt1 fails, such that you end up in the IDisposable.Dispose of your AsyncDisposer, there's no guarantee that dt2 has already completed, and thus if you need to wait until dt2 has completed (in order to either dispose of the returned IDisposable or propagate the exception), you'll need to synchronously block waiting for the operation to complete... that may or may not be acceptable to your needs (e.g. maybe exceptions are really rare here, and the extra bit of blocking in this case won't have any noticeable impact).

    If you don't need to do anything with the result of dt1 before awaiting dt2, you could use Task.WhenAll to make this easier, e.g.

    Task<IDisposable> dt1 = Async1();
    Task<IDisposable> dt2 = Async2();
    try
    {
        var disposables = await Task.WhenAll(dt1, dt2);
        using(IDisposable d1 = disposables[0])
        using(IDisposable d2 = disposables[1])
        {
            ... // work with d1 and d2 here
        }
    }
    catch
    {
        if (dt1.Status == TaskStatus.RanToCompletion) dt1.Result.Dispose();
        if (dt2.Status == TaskStatus.RanToCompletion) dt2.Result.Dispose();
        throw;
    }

    And you could encapsulate that into an async helper method which accepted the "work with d1 and d2 here" as a delegate to execute, e.g.

    static async Task RunAndDisposeAsync(
        Task<IDisposable> first, Task<IDisposable> second,
        Func<IDisposable,IDisposable,Task> func)
    {
        try
        {
            var disposables = await Task.WhenAll(first, second);
            using (IDisposable d1 = disposables[0], d2 = disposables[1])
                await func(d1, d2);
        }
        catch
        {
            if (first.Status == TaskStatus.RanToCompletion) first.Result.Dispose();
            if (second.Status == TaskStatus.RanToCompletion) second.Result.Dispose();
            throw;
        }
    }

    which in your example you'd then use like:

    await RunAndDisposeAsync(Async1(), Async2(), async (d1,d2) =>
    {
        ... // work with d1 and d2 here
    }); 

    You could also deal with this completely asynchronously by expanding out your original code to not use "using", though that will of course be some more code.

    Thursday, November 29, 2012 5:32 PM
    Moderator
  • Thanks, Stephen. Those were decent recommendations, but I've actually gone a bit of a different route since my posting, and have made a Task wrapper which is used as such:

    using(var t1 = Async1().ToDisposer())
    using(var t2 = Async2().ToDisposer())
    {
    	var t1r = await t1;
    	var t2r = await t2;
    }

    Where ToDisposer() returns a GetAwaiter-implementing struct which disposes of the Task's result. It's still a little more verbose than I'd like, but it's the best I've been able to come up with.

    One fundamental issue I'm trying to solve is that if for some reason Async2() throws (directly, as in, not returning a Task with an exception), I want to make sure Async1()'s result will be disposed.

    Thursday, November 29, 2012 6:41 PM
  • Hi scalablecory-

    With your solution, I'm still not clear on how it avoids blocking in Dispose... can you clarify?  If awaiting t1 throws an exception, you'll end up in the synchronous t2.Dispose method, and the Task wrapped by it may not have completed in that case... are you blocking then waiting for the Task to complete?

    Thursday, November 29, 2012 7:54 PM
    Moderator
  • Yea, I know it's not perfect. It doesn't avoid blocking on Dispose() -- it assumes exceptions are actually an exceptional case, and is just put there as a last-chance safety measure.

    I could, of course, make a WhenAll() that works for my Disposer objects to get around this issue.

    I wish there was a succinct and safely scalable solution to this, but it seems like you only get to pick one. Lucky me it's pretty rare to need to do this.

    • Edited by scalablecory Thursday, November 29, 2012 8:48 PM
    Thursday, November 29, 2012 8:22 PM
  • Ok, got it :)
    Thursday, November 29, 2012 10:53 PM
    Moderator