-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Implement IAsyncDisposable on various BCL types #27559
Comments
What's the guidance on throwing exceptions? Async disposal will often involve IO. |
It should be the same as for Dispose, for the same reasons plus consistency with Dispose. Dispose also often involves I/O. |
How about Brotli and Deflate compression? We have asks like this: aspnet/BasicMiddleware#247 and also lots of other issues (like threadpool exhaustion, and other subtle bugs) |
cc @Tratcher |
That's covered by Stream and "Then, our various Stream-derived types will provide a more specialized implementation where appropriate". |
Add extension method? public static ValueTask DisposeAsync(this Stream stream)
{
if (stream is IAsyncDisposable ad)
{
return ad.DisposeAsync();
}
return Task.Run(() => stream.Dispose());
} Would be overridden if derived type implements it and is referred to directly (like |
Even if We spoke in the meeting today about an alternative where we expose a protected virtual FlushFinalAsync; the base Stream.DisposeAsync would be non-virtual, and would await FlushFinalAsync then call Dispose(), the idea being that FlushFinalAsync would do all of the asynchronous work, such that the synchronous Dispose wouldn't have to do any I/O. At first that seemed like a good idea, but as I've gone through to implement it, I'm not sure it's actually any better than the previous solution. FlushFinalAsync will end up flushing such that subsequent flushes should be a nop, but if it doesn't dispose the stream, then the subsequent call to Dispose will have to invoke flush again (so two flushes instead of two disposes), and if it does dispose the stream, then it's no different than the two disposes. Plus, having the method as protected causes issues for wrappers like Streamwriter, whose equivalent FlushFinalAsync wouldn't have access to the protected virtual on Stream, and thus would be forced to use DisposeAsync in its FlushFinalAsync, which would then cause problems for its Dispose that would try to flush a disposed stream. This all makes me wonder whether the best solution is just a small variation of what I currently have up in the PR: have the base public virtual DisposeAsync on Stream just synchronously invoke Dispose. Yes, in some cases it'll end up doing I/O as part of DisposeAsync, but it'll generally be only a small amount of I/O, especially if the consuming code has already flushed (in which case it'll generally be no I/O)… plus it's not really any worse than the only option code has today. And a derived implementation can then just delegate to base; it already needs to be able to handle Dispose being called any number of times, so we'll incur potentially an extra Dispose invocation, but that should return immediately with minimal fanfare, and by having the invocation be synchronous, we don't have to pay for a queued work item in that case. |
If there is any additional flushing done it should not force disk access. Rather, it should just empty internal .NET buffers. This is something to keep in mind. |
That would of course be the case in any implementations we can control. |
Synchronously invoking |
It doesn't look like we updated the ref assembly to match the type definition (i.e. types now implement For example dotnet/corefx#33410 exposed the DisposeAsync method but didn't update the type signature
Is this intentional? It doesn't look like we expose IAsyncDisposable publicly in any contract either. |
No, that's an oversight on my part. Will fix. Apparently I added the DisposeAsync methods in the refs but didn't actually mark the types as implementing the interface. |
Related to #27547.
Now that we’re adding
System.IAsyncDisposable
to the core libraries, we also want to implement the interface on a variety of types that currently do or have the potential to do asynchronous work as part of their disposal. This is primarily focused onSystem.IO
types that might flush or otherwise perform I/O as part of their clean up (e.g. flushing a buffer), though it is not limited to such types.The following types would all implement IAsyncDisposable, and all gain a public member:
with it being virtual on all of the non-sealed classes:
DisposeAsync
will do the equivalent of Task.Run(Dispose). Then, our variousStream
-derived types will provide a more specialized implementation where appropriate. For example,MemoryStream
’sDispose
is a nop, so we’ll make itsDisposeAsync
a nop as well (unless the instance is actually of a type derived fromMemoryStream
, in which case we’ll delegate to the base implementation). Conversely,FileStream
’sDispose
does a flush, so itsDisposeAsync
will do an asynchronous flush.BinaryWriter
or something derived from it. If concrete, theDisposeAsync
will effectively be a copy of the synchronousDispose
, except using async equivalents on the underlyingStream
, e.g. where the synchronous implementation callsFlush
orDispose
/Close
, the asynchronous implementation would useFlushAsync
/DisposeAsync
. If instead the type is derived fromBinaryWriter
, the implementation will simply do the equivalent ofTask.Run(Dispose)
, so as to pick up whateverDispose
implementation the derived class is providing, and the derived class may then choose to overrideDisposeAsync
to provide a better implementation if applicable (the core libraries don’t provide any derived types).Task.Run(Dispose)
. Derived implementations can then do something better if appropriate. For example, we’ll override onStreamWriter
to asynchronously flush.Timer
currently provides twoDispose
methods,Dispose()
andDispose(WaitHandle)
, the latter of which not only stops the timer, but also signals the providedWaitHandle
when the timer guarantees that no more callbacks associated with that timer will be invoked. A caller can then block on thisWaitHandle
to know when it’s safe to progress with any state thatTimer
may have interacted with. As such, we’ll provide an equivalentDisposeAsync
, where the returnedValueTask
will complete when the timer appropriately guarantees the same thing, allowing a caller to wait asynchronously instead of synchronously.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.Open issues:
BinaryReader/TextReader. I listed
BinaryWriter
andTextWriter
, but notBinaryReader
andTextReader
. Do we want to implementIAsyncDisposable
on those as well, as with their writing counterparts? It’s rare for an implementation to actually need to do asynchronous work as part of closing a reader, as they generally don't need to flush. We could add them now for completeness, or we could wait until there's a demonstrated need.Stream.DisposeAsync. There are some unfortunate issues here. I see three main options:
FlushAsync
+Dispose
.(2) isn't a very good for a few reasons:
using
with an object, the compiler needs to be able to see statically that it implementsIDisposable
; similarly forIAsyncDisposable
withawait using
. It's very common to just have aStream
reference that you get handed from somewhere, and so even if the derived implementation implements the interface, you wouldn't be able to useawait using
with it as a Stream.BaseStream
implements the interface explicitly, then there's no good way for aDerivedStream : BaseStream
to invoke the base stream's implementation in order to clean up any resources the base stream owned.IAsyncDisposable
to know whether they can useDisposeAsync
and then fall back to usingDispose
ifDisposeAsync
isn't available.(1) is what I've implemented, but it has its own problems. First, when we add
DisposeAsync
toStream
, it has to invoke the existingDispose
, as otherwise when code started usingawait using
with a stream instead ofusing
, it wouldn't actually clean anything up until the owner of that stream released a new version that overrode the new method. Second, sinceDispose
could be doing anything, including I/O, we don't want to synchronously block the caller who just asked to do work asynchronously, soDisposeAsync
really needs to queue the call toDispose
. Now, it's very common for a derivedStream
'sDispose(bool)
override to do some cleanup and then callbase.Dispose(disposing)
in order to do whatever further cleanup work its base has (which may be an intermediary stream rather thanStream
itself). However, if you do that withDisposeAsync
and get down to the baseStream.DisposeAsync
method, you'll end up queueing a work item to invokeDispose
... this shouldn't hurt anything functionally, asDispose
is meant to be idempotent, but it's unnecessary work. We could say "if you derive directly fromStream
, don't bother calling tobase.Dispose(disposing)", but that doesn't work, either. Consider a type like
FileStream.
FileStreamhas its own cleanup to do, so it needs to override
DisposeAsync. However, what about an existing
MySpecializedFileStreamthat derives from
FileStreamand overrides
Disposeto cleanup additional stuff and then call to
FileStream's implementation. That
FileStream-derived type won't have overridden
DisposeAsyncyet, which means
FileStream's
DisposeAsyncactually needs to type test whether the instance is a concrete
FileStreamor something derived, and if derived, it should just use
base.DisposeAsync()instead of its async logic. That then makes the
baseinvocation problem transitive, as when
MySpecializedFileStreamgoes to override
DisposeAsync, if it calls
base.DisposeAsync(), it'll end up going all the way down to
Stream.DisposeAsync, which will queue a call to
Dispose`.This makes me wonder whether we should just do (3). But not implementing
IAsyncDisposable
on a type likeStream
makes me questionIAsyncDisposable
.Similar problems apply to
TextWriter
as well.A few notes:
IDisposable
also implementIAsyncDisposable
? No. The vast majority of IDisposable types do not perform asynchronous work as part of their disposal, with most dispose routines primarily focused on releasing native resources (often via calling Dispose on SafeHandles) and other such synchronous operations. We only want to implement IAsyncDisposable when we know that the type has the strong potential to do asynchronous I/O that would otherwise force its synchronous Dispose to block or spin waiting for those operations to complete.The text was updated successfully, but these errors were encountered: