Search code examples
c#idisposableiasyncdisposable

IAsyncDisposable: should DisposeAsyncCore call Dispose(false)?


In the DisposeAsync documentation, you have the following:

class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
    ...

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }

Specifically, they call Dispose(false) in DisposeAsync.

As I was missing that, I added it to the actual code.

I then started getting random 0xC0000374 (STATUS_HEAP_CORRUPTION).

These would occur as the class frees the pointer a second time.

I tried adding numerous if (Disposed) return; guards but that didn't fix.

If I remove it the problem is gone... but something seems to be missing now.

Obviously, I use await using, it's for making an overlapped DeviceIoControl as a Task.

Example:

using System.Runtime.InteropServices;

namespace Whatever.Extensions;

public sealed class NativeMemory : IDisposable, IAsyncDisposable
{
    private readonly int Length;

    private readonly nint Pointer;

    private bool Disposed;

    public NativeMemory(int length)
    {
        Pointer = Marshal.AllocHGlobal(Length = length);
    }

    public Span<byte> Span
    {
        get
        {
            unsafe
            {
                return new Span<byte>((void*)Pointer, Length);
            }
        }
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);
        Dispose(false); // BUG when added
        GC.SuppressFinalize(this);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private async ValueTask DisposeAsyncCore()
    {
        Marshal.FreeHGlobal(Pointer);

        await ValueTask.CompletedTask;
    }

    ~NativeMemory()
    {
        Dispose(false);
    }

    private void Dispose(bool disposing)
    {
        if (Disposed)
        {
            return;
        }

        Marshal.FreeHGlobal(Pointer);

        if (disposing)
        {
            // NOP
        }

        Disposed = true;
    }
}

Question:

How wrong is my implementation?


Solution

  • When problematic line is uncommented, you call Marshal.FreeHGlobal(Pointer) twice:

    public async ValueTask DisposeAsync()
    {
        // 1st call of Marshal.FreeHGlobal
        await DisposeAsyncCore().ConfigureAwait(false);
        // 2nd call of Marshal.FreeHGlobal
        Dispose(false); // BUG when added
        GC.SuppressFinalize(this);
    }
    

    Assuming that disposal is fast and we actually don't want async version, but interface implementation only, why don't we call Dispose(true) within DisposeAsync()?

    // let's not create any state machine with async + await:
    // we free resources synchronously and return completed task 
    public ValueTask DisposeAsync()
    {
       // Dispose instance properly 
       Dispose(true);
    
       // Suppress finalization
       GC.SuppressFinalize(this);
    
       // return completed task
       return ValueTask.CompletedTask;
    }
    

    Please note, that since your class is sealed you don't have to implement full IDisposable pattern (there's no use of Dispose(bool disposing) since the class will never be inherited):

    public NativeMemory(int length) {
        ArgumentOutOfRangeException.ThrowIfNegative(length);
    
        Pointer = Marshal.AllocHGlobal(Length = length);
    
        // Let GC know, that we allocated some memory
        GC.AddMemoryPressure(Length);
    }
    
    public void Dispose() {
        if (Disposed)
            return;
    
        Marshal.FreeHGlobal(Pointer); 
        // We release memory allocated, let GC know about it
        GC.RemoveMemoryPressure(Length);  
    
        GC.SuppressFinalize(this);
    
        Disposed = true;
    }
    
    ~NativeMemory() {
        Dispose();
    }
    
    public ValueTask DisposeAsync() {
       // Dispose instance properly 
       Dispose();
    
       // return completed task
       return ValueTask.CompletedTask;
    }