BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Update on IAsyncDisposable and IAsyncEnumerator

Update on IAsyncDisposable and IAsyncEnumerator

This item in japanese

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.

Rate this Article

Adoption
Style

BT