Реализация метода DisposeAsync

Интерфейс System.IAsyncDisposable впервые появился в составе C# 8.0. Метод IAsyncDisposable.DisposeAsync() реализуется, когда нужно выполнить очистку ресурса. Для этих целей также реализуется метод Dispose. Однако одним из ключевых различий является то, что эта реализация позволяет выполнять асинхронные операции очистки. Возвращает объект DisposeAsync() , ValueTask представляющий асинхронную операцию удаления.

Обычно при реализации IAsyncDisposable интерфейса, который классы также реализуют IDisposable интерфейс. Хороший шаблон IAsyncDisposable реализации интерфейса заключается в том, чтобы быть готовым к синхронному или асинхронному удалению, однако это не обязательно. Если синхронный удаленный класс недоступен, IAsyncDisposable он может быть допустимым. Все рекомендации по реализации шаблона удаления также применяются к асинхронной реализации. В этой статье предполагается, что вы уже знаете, как реализовать метод Dispose.

Внимание

Если вы реализуете IAsyncDisposableIDisposable интерфейс, но не интерфейс, приложение может потенциально утечки ресурсов. Если класс реализует IAsyncDisposable, но не IDisposableвызывает Disposeтолько потребитель, реализация никогда не будет вызываться DisposeAsync. Это приведет к утечке ресурсов.

Совет

Что касается внедрения зависимостей, при регистрации служб в ней IServiceCollectionвремя существования службы управляется неявно от вашего имени. Очистка и соответствующая IServiceProviderIHost очистка ресурсов оркестрации. В частности, реализации IDisposable и IAsyncDisposable правильно удаляются в конце указанного времени существования.

Дополнительные сведения см. в статье Внедрение зависимостей в .NET.

Изучение DisposeAsync и DisposeAsyncCore методы

Интерфейс IAsyncDisposable объявляет единственный метод без параметров — DisposeAsync(). Любой DisposeAsyncCore() незаверованный класс должен определять метод, который также возвращает ValueTaskобъект.

  • Реализация publicIAsyncDisposable.DisposeAsync() , которая не имеет параметров.

  • Метод protected virtual ValueTask DisposeAsyncCore() со следующей сигнатурой:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

Метод DisposeAsync

Метод DisposeAsync() с атрибутом public без параметров вызывается неявно в инструкции await using, и его назначение состоит в том, чтобы освободить неуправляемые ресурсы, выполнить общую очистку и указать, что метод завершения, если он задан, не нужно выполнять. Освобождение памяти, связанной с управляемым объектом, всегда оставляется сборщику мусора. Он имеет стандартную реализацию:

public async ValueTask DisposeAsync()
{
    // Perform async cleanup.
    await DisposeAsyncCore();

    // Dispose of unmanaged resources.
    Dispose(false);

    // Suppress finalization.
    GC.SuppressFinalize(this);
}

Примечание.

Основным отличием шаблона асинхронного освобождения от шаблона освобождения является то, что когда из метода DisposeAsync() выполняется вызов метода перегрузки Dispose(bool), в качестве аргумента передается значение false. В то же время при реализации метода IDisposable.Dispose() вместо него передается значение true. Это помогает обеспечить функциональную эквивалентность с шаблоном синхронного освобождения, а также гарантирует, что пути кода метода завершения по-прежнему вызываются. Другими словами, метод DisposeAsyncCore() будет освобождать управляемые ресурсы асинхронно, поэтому вы не захотите, чтобы они также освобождались и синхронно. Следовательно, вызывайте Dispose(false) вместо Dispose(true).

Метод DisposeAsyncCore

Метод DisposeAsyncCore() предназначен для выполнения асинхронной очистки управляемых ресурсов или для каскадных вызовов DisposeAsync(). Он инкапсулирует общие асинхронные операции очистки, когда подкласс наследует базовый класс, который является реализацией IAsyncDisposable. Метод DisposeAsyncCore() заключается в том virtual , чтобы производные классы могли определять настраиваемую очистку в их переопределениях.

Совет

Если реализация IAsyncDisposable — sealed, то метод DisposeAsyncCore() не требуется, а асинхронная очистка может выполняться непосредственно в методе IAsyncDisposable.DisposeAsync().

Реализация шаблона асинхронного освобождения

Все нерегламентированные классы должны рассматриваться как потенциальный базовый класс, так как они могут быть унаследованы. При реализации шаблона асинхронного освобождения для любого потенциального базового класса вы должны предоставить метод protected virtual ValueTask DisposeAsyncCore(). В некоторых из следующих примеров используется класс, определенный NoopAsyncDisposable следующим образом:

public sealed class NoopAsyncDisposable : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}

Ниже приведен пример реализации асинхронного шаблона удаления, использующего NoopAsyncDisposable тип. Тип реализуется DisposeAsync путем ValueTask.CompletedTaskвозврата.

public class ExampleAsyncDisposable : IAsyncDisposable
{
    private IAsyncDisposable? _example;

    public ExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

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

        GC.SuppressFinalize(this);
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_example is not null)
        {
            await _example.DisposeAsync().ConfigureAwait(false);
        }

        _example = null;
    }
}

В предыдущем примере:

  • Это ExampleAsyncDisposable ненаправленный класс, реализующий IAsyncDisposable интерфейс.
  • Он содержит частное IAsyncDisposable поле, _exampleкоторое инициализировано в конструкторе.
  • Метод DisposeAsync делегирует DisposeAsyncCore метод и вызывает GC.SuppressFinalize , чтобы уведомить сборщика мусора о том, что методу завершения не нужно выполняться.
  • Он содержит DisposeAsyncCore() метод, который вызывает _example.DisposeAsync() метод, и задает для поля значение null.
  • Этот DisposeAsyncCore() метод позволяет virtualподклассам переопределять его с помощью пользовательского поведения.

Запечатанный альтернативный асинхронный шаблон удаления

Если вы можете sealedреализовать класс, можно реализовать шаблон асинхронного удаления, переопределив IAsyncDisposable.DisposeAsync() метод. В следующем примере показано, как реализовать шаблон асинхронного удаления для запечатаемого класса:

public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
    private readonly IAsyncDisposable _example;

    public SealedExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public ValueTask DisposeAsync() => _example.DisposeAsync();
}

В предыдущем примере:

  • Это SealedExampleAsyncDisposable запечатанный класс, реализующий IAsyncDisposable интерфейс.
  • _example Содержащее поле инициализировано readonly в конструкторе.
  • Метод DisposeAsync вызывает _example.DisposeAsync() метод, реализуя шаблон через содержащее поле (каскадное удаление).

Реализация шаблонов освобождения и асинхронного освобождения

Может потребоваться реализовать оба интерфейса IDisposable и IAsyncDisposable, особенно если область класса содержит экземпляры этих реализаций. Это гарантирует правильную каскадную очистку вызовов. Ниже приведен пример класса, реализующего оба интерфейса и демонстрирующий надлежащее руководство по очистке.

class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
    IDisposable? _disposableResource = new MemoryStream();
    IAsyncDisposable? _asyncDisposableResource = new MemoryStream();

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

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

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

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disposableResource?.Dispose();
            _disposableResource = null;

            if (_asyncDisposableResource is IDisposable disposable)
            {
                disposable.Dispose();
                _asyncDisposableResource = null;
            }
        }
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_asyncDisposableResource is not null)
        {
            await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }

        if (_disposableResource is IAsyncDisposable disposable)
        {
            await disposable.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
            _disposableResource?.Dispose();
        }

        _asyncDisposableResource = null;
        _disposableResource = null;
    }
}

Реализации IDisposable.Dispose() и IAsyncDisposable.DisposeAsync() являются простым шаблонным кодом.

В методе перегрузки Dispose(bool) экземпляр условно удаляется, IDisposable если это не nullтак. Экземпляр IAsyncDisposable создается как IDisposable, и если он также не nullявляется, он также удален. Затем оба экземпляра назначаются null.

В методе DisposeAsyncCore() используется один и тот же логический подход. IAsyncDisposable Если экземпляр не nullявляется, его вызов DisposeAsync().ConfigureAwait(false) ожидается. Если экземпляр IDisposable также является реализацией IAsyncDisposable, он также освобождается асинхронно. Затем оба экземпляра назначаются null.

Каждая реализация стремится удалить все возможные удаленные объекты. Это гарантирует правильность очистки.

Использование интерфейса асинхронного высвобождения

Чтобы правильно использовать объект, который реализует интерфейс IAsyncDisposable, следует использовать ключевые слова await и using вместе. Рассмотрим следующий пример. В нем создается экземпляр класса ExampleAsyncDisposable, который затем заключается в инструкцию await using.

class ExampleConfigureAwaitProgram
{
    static async Task Main()
    {
        var exampleAsyncDisposable = new ExampleAsyncDisposable();
        await using (exampleAsyncDisposable.ConfigureAwait(false))
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

Внимание

Для настройки порядка маршалирования продолжения задачи в ее исходном контексте или в планировщике используйте метод расширения ConfigureAwait(IAsyncDisposable, Boolean) интерфейса IAsyncDisposable. Дополнительные сведения о методе ConfigureAwait см. в разделе Вопросы и ответы по ConfigureAwait.

В ситуациях, когда использование ConfigureAwait не требуется, await using инструкция может быть упрощена следующим образом:

class ExampleUsingStatementProgram
{
    static async Task Main()
    {
        await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

Более того, ее можно написать так, чтобы неявно использовалась область объявления using.

class ExampleUsingDeclarationProgram
{
    static async Task Main()
    {
        await using var exampleAsyncDisposable = new ExampleAsyncDisposable();

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Несколько ключевое слово ожиданий в одной строке

await Иногда ключевое слово могут отображаться несколько раз в одной строке. Рассмотрим следующий пример кода:

await using var transaction = await context.Database.BeginTransactionAsync(token);

В предыдущем примере:

  • Метод BeginTransactionAsync ожидается.
  • Тип возвращаемого значения — DbTransactionэто тип, реализующий IAsyncDisposable.
  • Используется transaction асинхронно, а также ожидается.

Стекированные объявления using

Когда вы создаете и используете несколько объектов, реализующих IAsyncDisposable, стекирование операторов await using с помощью ConfigureAwait в ошибочных условиях может предотвратить вызовы DisposeAsync(). Чтобы метод DisposeAsync() вызывался всегда, следует избегать стекирования. В приведенных ниже трех примерах кода показаны допустимые шаблоны.

Допустимый шаблон 1


class ExampleOneProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.

            var objTwo = new ExampleAsyncDisposable();
            await using (objTwo.ConfigureAwait(false))
            {
                // Interact with the objOne and/or objTwo instance(s).
            }
        }

        Console.ReadLine();
    }
}

В предыдущем примере каждая асинхронная операция очистки явно область под блокомawait using. Внешний область следует, как objOne задает его фигурные скобки, заключивobjTwo, как такое objTwo удаление сначала, а затем objOne. Оба IAsyncDisposable экземпляра DisposeAsync() ожидают своего метода, поэтому каждый экземпляр выполняет асинхронную операцию очистки. Вызовы вложены, а не стекированы.

Допустимый шаблон 2

class ExampleTwoProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.
        }

        var objTwo = new ExampleAsyncDisposable();
        await using (objTwo.ConfigureAwait(false))
        {
            // Interact with the objTwo instance.
        }

        Console.ReadLine();
    }
}

В предыдущем примере каждая асинхронная операция очистки явно область под блокомawait using. В конце каждого блока IAsyncDisposable соответствующий экземпляр имеет DisposeAsync() ожидаемый метод, таким образом выполняя асинхронную операцию очистки. Вызовы являются последовательными, а не стекированы. В этом сценарии сначала удаляется objOne, а затем objTwo.

Допустимый шаблон 3

class ExampleThreeProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using var ignored1 = objOne.ConfigureAwait(false);

        var objTwo = new ExampleAsyncDisposable();
        await using var ignored2 = objTwo.ConfigureAwait(false);

        // Interact with objOne and/or objTwo instance(s).

        Console.ReadLine();
    }
}

В предыдущем примере каждая асинхронная операция очистки неявно область с текстом содержащего метода. В конце заключиющего блока IAsyncDisposable экземпляры выполняют асинхронные операции очистки. Этот пример выполняется в обратном порядке, из которого они были объявлены, то есть objTwo удаляется раньше objOne.

Недопустимый шаблон

Выделенные строки в следующем коде показывают, что означает наличие "стека с использованием". Если исключение создается из конструктора AnotherAsyncDisposable , ни тот объект не удаляется должным образом. Переменная objTwo никогда не назначается, так как конструктор не завершился успешно. В результате конструктор AnotherAsyncDisposable отвечает за удаление всех ресурсов, выделенных перед созданием исключения. Если тип ExampleAsyncDisposable имеет метод завершения, он имеет право на завершение.

class DoNotDoThisProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        // Exception thrown on .ctor
        var objTwo = new AnotherAsyncDisposable();

        await using (objOne.ConfigureAwait(false))
        await using (objTwo.ConfigureAwait(false))
        {
            // Neither object has its DisposeAsync called.
        }

        Console.ReadLine();
    }
}

Совет

Избегайте такого шаблона, так как он может привести к непредвиденному поведению. Если вы используете один из допустимых шаблонов, проблема нерасположенных объектов не существует. Операции очистки выполняются правильно, когда using операторы не стекаются.

См. также

Пример двойной реализации IDisposable и IAsyncDisposable, см. в исходном коде Utf8JsonWriterв GitHub.