В 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);
дважды — в DisposeAsyncCore()
, а затем в Dispose
, когда раскомментируете проблемную строку? Я предлагаю всегда проверять и устанавливать Disposed
на true
, независимо от того, звоните ли вы Dispose
или DisposeAsync
Установите Pointer
на IntPtr.Zero
после освобождения, чтобы двойной вызов не выполнялся. Вы всегда должны делать это на тот случай, если метод Dispose вызывается во второй раз.
Что ж, я следую документации, в которой, если вы не заметили, есть примечание фиолетового цвета о вызове Dispose(false)
в DisposeAsyncCore
. Другой вопрос: почему это работает в 99,9% случаев, а не в 0,01%? Это из-за какой-то гонки? Насколько я могу судить, я не делаю ничего особенного...
@Charlieface да, хотя при этом теряется аспект указателя, доступный только для чтения, это отличный подход.
Я думаю, настоящий вопрос заключается в том, зачем вообще использовать DisposeAsync
, что в этом такого асинхронного?
Когда проблемная строка раскомментирована, вы дважды вызываете 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;
}
Этого хватит, спасибо.
Почему ты звонишь
Marshal.FreeHGlobal(Pointer);
дважды?