Интерфейс IAsyncDisposable служит для асинхронного освобождения неуправляемых ресурсов (unmanaged resources). Появился он в версии .NET Core 3.0. В .NET классы, владеющие неуправляемыми ресурсами, обычно реализуют интерфейс IDisposable, чтобы обеспечить механизм синхронного освобождения неуправляемых ресурсов (см. подробности в посте Имплементация интерфейса IDisposable). Однако в некоторых случаях классам необходимо предоставлять асинхронный механизм освобождения неуправляемых ресурсов в дополнение к синхронному (или вместо него). Предоставление такого механизма позволяет потребителю выполнять ресурсоемкие операции удаления не блокируя основной поток приложения с графическим интерфейсом в течение длительного времени.
Метод IAsyncDisposable.DisposeAsync этого интерфейса возвращает ValueTask, представляющий асинхронную операцию освобождения ресурсов. Класс, владеющий неуправляемыми ресурсами, реализует этот метод, а потребитель класса вызывает метод DisposeAsync объекта, когда он больше не нужен. Чтобы ресурсы освобождались даже в случае исключения следует поместить код, использующий объект IAsyncDisposable, в оператор using (в C#, начиная с версии 8.0), или вызвать метод DisposeAsync внутри finally оператора try/finally.
Приведём пример кода, поясняющий вышеописанное:
1 | class SomeClass : IAsyncDisposable, IDisposable |
А где-то в приложении класс можно использовать так:
1 | static async Task Main() |
Код выше показал, в общих чертах, паттерн Async Dispose Pattern. Как следует из кода, asyncDisposeObj ссылается на ресурс который следует утилизировать асинхронно, а syncDisposeObj - синхронно. Теперь синхронная и асинхронная утилизация ресурсов могут выполняться в одно время, так что важно рассматривать эти процессы совместно. Для синхронной и асинхронной утилизации ресурсов класс реализует интерфейсы IAsyncDisposable и IDisposable. Как показано в посте Имплементация интерфейса IDisposable IDisposable должен определять метод Dispose() без параметров, виртуальный метод Dispose(bool) и опциональный финализатор (деструктор). Наш пример не требует опционального финализатора (деструктора). А для IAsyncDisposable требуется определить методы без параметров DisposeAsync() и DisposeAsyncCore(). Оба пути утилизации (синхронный и асинхронный) могут сработать, поэтому они оба должны быть готовы утилизировать ресурсы. В методе Dispose(bool) не только вызывается Dispose() для синхронного ресурса syncDisposeObj, но и произвоится попытка вызвать Dispose() для асинхронного asyncDisposeObj. Отметьте, что Dispose(bool) также вызывает DisposeThisObject, который содержит тот же код, что и аналогичный код для асинхронного пути во избежание дублирования.
Методы Dispose() и DisposeAsync() являются членами интерфейсов, а методы Dispose(bool) и DisposeAsyncCore() - конвенциями (условленными договоренностями). Оба последних метода виртуальные. Это является частью паттерна, когда производный класс может реализовать утилизацию ресурсов, переопределяя эти методы и вызывая их через base.Dispose(bool) и base.DisposeAsyncCore(), чтобы гарантировать освобождение ресурсов по всей иерархии наследования.
Оба метода Dispose() и DisposeAsync() вызывают Dispose(bool), но DisposeAsync() устанавливает флаг disposing в false. Напомню, что disposing = true это флаг утилизации управляемых ресурсов. Метод Dispose(bool) - это синхронный путь, а метод DisposeAsync() вызывает DisposeAsyncCore() для утилизирования асинхронных ресурсов. Как и Dispose(true) метод DisposeAsyncCore() пытается высвободить все управляемые ресурсы. Асинхронный случай очевиден, однако синхронный имеет пару особенностей. Что если синхронный объект сейчас или в будущем реализует IAsyncDisposable? Тогда попытка вызова DisposeAsync() является наилучшим выбором в случае асинхронного пути выполнения кода. Иначе вызовется синхронный путь выполнения с методом Dispose().
В конце отметим, что метод Main использует конструкцию await using при создании экземпляра класса реализующего интерфейсы IAsyncDisposable и IDisposable. Это гарантирует вызов метода DisposeAsync() по завершению выполнения метода Main.