Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the recommended way to deal with leaked IAsyncDisposable instances?

I've been familiarizing myself with some of the things (that are planned to be) added in C# 8 & .NET Core 3.0, and am unsure on the correct way to implement IAsyncDisposable (at time of writing, this link has literally no guidance).

In particular, it is unclear to me what to do in the case when an instance isn't explicitly disposed - that is, it isn't wrapped in an async using(...) and .DisposeAsync() isn't explicitly called.

My first thought was to do that same thing I'd do when implementing IDisposable:

  • My DisposeAsync() implementation calls a DisposeAsync(bool disposing) with disposing: true
  • Implement a finalizer (with ~MyType()) that calls DisposeAsync(disposing: false)
  • DisposeAsync(bool disposing) actually frees and/or disposes everything, and suppresses finalizing if disposing == true.

My concerns are that there's nothing to await the results of DisposeAsync(bool) in the finalizer, and explicitly waiting in a finalizer seems really really dangerous.

Of course "just leak" also seems less than ideal.

To make this concrete, here's a (simplified) example class that does have a finalizer:

internal sealed class TestAsyncReader: IAsyncDisposable
{
    private bool IsDisposed => Inner == null;
    private TextReader Inner;
    internal TestAsyncReader(TextReader inner)
    {
        Inner = inner;
    }

    // the question is, should this exist?
    ~TestAsyncReader()
    {
        DisposeAsync(disposing: false);
    }

    private ValueTask DisposeAsync(bool disposing)
    {
        // double dispose is legal, but try and do nothing anyway
        if (IsDisposed)
        {
            return default;
        }

        // should a finalizer even exist?
        if (disposing)
        {
            GC.SuppressFinalize(this);
        }

        // in real code some resources explicitly implement IAsyncDisposable,
        //   but for illustration purposes this code has an interface test
        if (Inner is IAsyncDisposable supportsAsync)
        {
            var ret = supportsAsync.DisposeAsync();
            Inner = null;
            return ret;
        }

        // dispose synchronously, which is uninteresting
        Inner.Dispose();
        Inner = null;
        return default;
    }

    public ValueTask DisposeAsync()
    => DisposeAsync(disposing: true);
}

So, is there any guidance around proper handling of leaked IAsyncDisposable instances?

like image 504
Kevin Montrose Avatar asked Apr 14 '19 16:04

Kevin Montrose


1 Answers

Basing on examples of how it's implemented inside .NET Core classes (like here) and some recommendations from there, I'd say that when you need to implement IAsyncDisposable, the good practice would be to implement both IAsyncDisposable and IDisposable. In this case IAsyncDisposable will be only responsible for explicit scenarios when asyncronus disposal is needed, while IDisposable is supposed to be implemented as usual according to disposable pattern practices and it's going to serve all fallback scenarios including those when things come to finalization. Thus you don't need to have anything like DisposeAsync(bool disposing) - the asynchronous disposal cannot and shouldn't happen in a finalizer. The only bad news that you'll have to support both paths for resources reclaiming (synchronous and asynchronous).

like image 112
Dmytro Mukalov Avatar answered Nov 10 '22 09:11

Dmytro Mukalov