IAsyncDisposable: следует ли DisposeAsyncCore вызывать Dispose(false)?

В DisposeAsyncдокументации указано следующее:

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

Конкретно звонят Dispose(false) в DisposeAsync.

Поскольку мне этого не хватало, я добавил это в реальный код.

Затем я начал получать случайные 0xC0000374 (STATUS_HEAP_CORRUPTION).

Это произойдет, когда класс освободит указатель во второй раз.

Я пытался добавить множество if (Disposed) return; охранников, но это не помогло.

Если я удалю его, проблема исчезнет... но теперь чего-то не хватает.

Очевидно, я использую await using, чтобы сделать перекрывающуюся DeviceIoControl в качестве Task.

Пример:

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

Вопрос:

Насколько ошибочна моя реализация?

Почему ты звонишь Marshal.FreeHGlobal(Pointer); дважды?

Progman 17.07.2024 22:08

Разве вы не пытаетесь вызвать Marshal.FreeHGlobal(Pointer); дважды — в DisposeAsyncCore(), а затем в Dispose, когда раскомментируете проблемную строку? Я предлагаю всегда проверять и устанавливать Disposed на true, независимо от того, звоните ли вы Dispose или DisposeAsync

Dmitry Bychenko 17.07.2024 22:09

Установите Pointer на IntPtr.Zero после освобождения, чтобы двойной вызов не выполнялся. Вы всегда должны делать это на тот случай, если метод Dispose вызывается во второй раз.

Charlieface 17.07.2024 23:13

Что ж, я следую документации, в которой, если вы не заметили, есть примечание фиолетового цвета о вызове Dispose(false) в DisposeAsyncCore. Другой вопрос: почему это работает в 99,9% случаев, а не в 0,01%? Это из-за какой-то гонки? Насколько я могу судить, я не делаю ничего особенного...

aybe 18.07.2024 02:10

@Charlieface да, хотя при этом теряется аспект указателя, доступный только для чтения, это отличный подход.

aybe 18.07.2024 02:10

Я думаю, настоящий вопрос заключается в том, зачем вообще использовать DisposeAsync, что в этом такого асинхронного?

Charlieface 18.07.2024 02:20
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
6
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Когда проблемная строка раскомментирована, вы дважды вызываете Marshal.FreeHGlobal(Pointer):

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

Предполагая, что удаление происходит быстро и нам на самом деле нужна не асинхронная версия, а только реализация интерфейса, почему бы нам не вызвать Dispose(true) внутри 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;
}

Обратите внимание: поскольку ваш класс sealed, вам не нужно реализовывать полный IDisposable шаблон (в Dispose(bool disposing) нет смысла, поскольку класс никогда не будет унаследован):

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

Этого хватит, спасибо.

aybe 18.07.2024 02:17

Другие вопросы по теме