Part of the async streams proposal is the ability to asynchronously dispose a resource. This interface is called IAsyncDisposable
and has a single method called DisposeAsync
. The first thing you may notice is the naming convention. Historically, the guidance from Microsoft is that asynchronous methods should end with the suffix "Async". By contrast, the recommendation for types (i.e. classes and interfaces) is to use "Async" as a prefix.
As a potential performance improvement, DisposeAsync
will return a ValueTask instead of a normal Task object. And because it may lead to instability, you will not be able to pass a cancellation token to the DisposeAsync
method.
IDisposable vs IAsyncDisposable
IAsyncDisposable
does not inherit from IDisposable
, allowing developers to choose between implementing one or both interfaces. That said, the current theory is it is rare for a class to offer both interfaces.
Microsoft recommends that if you do have both, the class should allow Dispose
and DisposeAsync
to be called in either order. Only the first call will be honored, with subsequent calls to either method being a no-op. (This is only a recommendation; specific implementations may differ.)
Asynchronously Disposable Classes
Several classes have been singled out as needing asynchronous disposal support in .NET Core 3.0.
The first is Stream
. This base class is used for a variety of scenarios and is often subclassed in third party libraries. The default implementation of AsyncDispose
will be to call Dispose
on a separate thread. This is generally considered to be a bad practice, so subclasses should override this behavior with a more appropriate implementation.
Likewise, a BinaryReader
or TextReader
will call Dispose
on a separate thread if not overridden.
Threading.Timer has already implemented IAsyncDisposable as part of .NET Core 3.0.
CancellationTokenRegistration:
CancellationTokenRegistration.Dispose does two things: it unregisters the callback, and then it blocks until the callback has completed if the callback is currently running. DisposeAsync will do the same thing, but allow for that waiting to be done asynchronously rather than synchronously.
Changes to IAsyncEnumerator
Since we last reported on IAsyncEnumerator
, the MoveNextAsync
method has also been changed to return a ValueTask<bool>
. This should allow for better performance in tight loops where MoveNextAsync
will return synchronously most of the time.
Alternate Design for IAsyncEnumerator
An alternate design for IAsyncEnumerator
was considered. Rather than a Current property like IEumerator
, it would expose a method with this signature:
T TryGetNext(out bool success);
The success parameter indicates whether or not a value could be read synchronously. If it is false, the result of the function should be discarded and MoveNextAsync
called to wait for more data.
Normally a try method returns a Boolean and the actual value is in an out parameter. However, that makes the method non-covariant (i.e. you cannot use IAsyncEnumerator<out T>
), so the non-standard ordering was necessary.
This design was placed on hold for two reasons. First, the performance benefits during testing were not compelling. Using this pattern does improve performance, but those improvements are likely to be very minor in real world usage. Furthermore, this pattern is much harder to correctly use from both the library author's and the client's perspectives.
If the performance benefit is determined to be significant at a later date, then an optional second interface can be added to support that scenario.
Cancellation Tokens and Async Enumeration
It was decided that the IAsyncEnumerable<T>
/IAsyncEnumerator<T>
would be cancellation-agnostic. This means they cannot accept a cancellation token, nor would the new async aware for-each syntax have a way to directly consume one.
This doesn't mean you cannot use cancellation tokens, only that there isn't any special syntax to help you. You can still explicitly call ThrowIfCancellationRequested
or pass a cancellation token to an iterator.
For-each Loops
For-each loops will continue to use the synchronous interfaces. In order to use the async enumerator, you must use the syntax below:
await foreach (var i in enumerable)
Note the placement of the await keyword. If you were to move it as shown in the next example, then getting the collection would be asynchronous, but the for-each loop itself would be synchronous.
foreach (var i in await enumerable)
Like a normal for-each loop, you do not need to actually implement the enumerator interfaces. If you expose instance (or extension) methods that match the async for-each pattern, they will be used even if the interfaces were also offered.