-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
IAsyncDisposable, using statements, and async/await #114
Comments
I find it scary that there'll be an implicit "await" in your code (i.e. re-entrancy vulnerability) with no indication of it in your code. Could you tell me your thoughts on a compound keyword?
|
I like the idea and I prefer it with @ljw1004 addition. |
@giggio, ah, excellent point. That's what sunk the idea when we thought about it a few years ago. The point is that a library would always want
but a user app would always want it without the ConfigureAwait. |
I don't have an answer for you yet, but I accept your challenge 😎 |
@terrajobst It would be great to merge this proposal with the one that you did a few years back? |
I'll dig it up and see what additional thoughts we had back then but as far as I remember, @sharwell pretty much nailed it already. |
Ditto. |
I propose an extension method be provided for the type public static T ConfigureDispose<T>(this T disposable, bool continueOnCapturedContext)
where T : IAsyncDisposable This would allow you to do the following: using (ResourceType resource = expression.ConfigureDispose(false))
{
...
} |
I hope that expansions are conceptual but not actual, because AFAIK original "using" statement does not perform boxing to a disposable "struct" |
@zahirtezcan The original post uses language and examples very similar to the actual C# Language Specification. The expansions define behavior; a compiler is free to emit IL code that differs from this source code provided the observable behavior matches the definition. The primary case where this happens today involves avoiding boxing of struct types. |
In my apps, every method call that might go over the network or disk is one that will fail at some point during normal healthy execution of my program and I have to cope with it. So I wrote two NuGet packages (AsyncStackTraceEx and AsyncMrFlakey) so I can write Await FooAsync().Log("a") ' uses a library that gives better Exception.StackTrace in case of failure
Await FooAsync().Flakey() ' lets me simulate a network/disk failure during my testing I guess I'd now have to add overloads for .Log and .Flakey which take an IAsyncDisposable, as well as the existing overloads that take Task and Task and IAsyncAction and IAsyncOperation. |
I think this is not a pattern that should be promoted. Your code should not typically rely on the result of a dispose method, and your dispose should not be long running. If you need to do lots of work in your dispose, then it's already a red flag. If that's the case, then you should just do a Task.Run() inside the dispose and return immediately. Microsoft has been extremely apprehensive to support situations where you might implicitly block a method for an indeterminate amount of time. |
I concur with @kbirger on this. The intent of a Dispose method is to release a resource opportunistically, and resource teardown should almost always be synchronous / short-running. Allowing async disposal seems to encourage the "perform some mandatory non-resource-teardown action at this deterministic time" pattern. In async methods in particular this pattern could lead to a pit of failure because (a) the async callback by definition does not run at a deterministic point and (b) it's not even guaranteed to run at all. (I'm speaking only for myself, not any team within Microsoft.) |
@GrabYourPitchforks, I disagree. Think of a network connection with a server. It's reasonable that "disposing" of the connection would involve sending a message to the server to let it release its resources gracefully, and awaiting until the server has either acknowledged receipt of that notification, or torn down its resources. |
@ljw1004, in your example, it sounds like there's also a chance that the graceful bit won't happen, which means there could be an exception. This would be highly undesirable behavior. And again... if it's a potentially long-running task, it doesn't belong in a dispose method. If you must put it in a dispose method, then it should be fire-and-forget, which is to say Task.Run(). |
Just putting it out there -- even WCF Client (which you get from AddServiceReference) has "CloseAsync". |
|
@ljw1004 - I think these are different. An asynchronous close has its place, and you will always have the ability to create a closing method which returns a task, but that is different from having this done automatically in a using block. In what way is Another thought I have about the long-running aspect is, it seems that for clean-up code, you would want to be able to set a timeout. It sounds like it would be more elegant to just handle it using try-finally at this point, so that you can correctly handle the control logic for your cleanup. |
Something that might be of interest, esp. the rationale for various decisions: https://www.python.org/dev/peps/pep-0492/#asynchronous-context-managers-and-async-with |
I agree with using |
For real life use cases that fall outside what was mentioned, xUnit just added IAsyncFixture to allow for async initialization and disposal of fixture information: xunit/xunit#424 |
Just because it hasn't been mentioned as far as I've seen... should it take a cancellation token? That would potentially handle @kbirger's timeout aspect, although of course a cancellation token is advisory rather than "stomp on the thread"... |
Something like?: public interface IAsyncDisposable : IDisposable
{
Task DisposeAsync(CancellationToken token = default(CancellationToken));
} Edit: how would this work when being used inside a theoretical using... Would you just have to be explicit and unroll it yourself? |
@RichiCoder1 read the suggestion of @sharwell above which could be adapted to solve this elegantly and still use using: public static T WithCancellation<T>(this T disposable, CancellationToken token)
where T : IAsyncDisposable
using (var x = Fred().WithCancellation(c)) {
...
} |
I had no idea that the current documentation says this literally all over:
It's of course ridiculous. In the IDE, you're given quick actions to implement either the unmanaged IDisposable pattern or the purely managed pattern. |
Also, I am of the frame of mind that the unmanaged ( |
@ljw1004 With all the discussion and suggestions here, what is your current thought on what the syntax/rules should be for this proposal? Or is it too early to ask? |
@binki I'm no longer the champion for this feature (I switched job last fall). In my mind it remains vexing... I'm interesting in Kotlin continuations: https://github.com/Kotlin/kotlin-coroutines/blob/master/kotlin-coroutines-informal.md#continuation-interface -- these provide a uniform library-defined mechanism for writing any kind of resumable method (e.g. async methods, iterators, async iterators, ...). So someone could write await(...) just as a library method and it would do the same compiler transformation as C# does, or they could write yield(...) as a library method. So they must deal with the same issue as IAsyncDisposable. How do they deal with it? |
Hmm, Kotlin coroutines defines an extension method with a language-supported How they would handle // This would call the Task<T> overload of UsingAsync(). There should *not*
// be parens around the await.
await GetResourceAsync().UsingAsync(async resource => {
await resource.DoSomethingAsync();
}); …where Maybe if this pattern could be promoted to the BCL, that would be enough. No need for new confusing syntax since that was hard to agree upon? |
I do not want a lambda. You get capture issues and warnings, it looks ugly, it's hard to type, it's additional overhead that the |
@jnm2 Though, regarding capture issues, wouldn’t you have no issues beyond the same issues you have with |
Please consider another design /// <summary>
/// Asynchronously disposable.
/// </summary>
public interface IAsyncDisposable
{
/// <summary>
/// Dispose task. Returns valid dispose task even <see cref="StartDispose"/> was not called yet. <br/>
/// When something goes wrong and problem cannot be handled - task should be completed with unprocessable exception to hint
/// application crash.
/// </summary>
Task DisposeTask { get; }
/// <summary>
/// Initiates async disposing, allowed to be called multiple times. Should never <see langword="throw"/> an exception.
/// </summary>
void StartDispose();
} I found this design many times more suitable in actual framework building. But possibly both design should coexist (for different purposes). |
One big change to original proposal is the ability to have valid "Task" that can be used for continuations and regardless of dispose started or not. |
@dmitriyse A class desiring such “only start the disposable once” behavior could just return the same
|
Hi! Yes, returning the same Task from AsyncDispose is a good and possible idea. And I also share your reasons. But one case is not covered by Task DisposeAsync(). You cannot get Task and not query disposing. |
I think requirement to have "dispose finished" Task-event (while don't do dispose) is not so rare . And if IAsyncDisposable will not support this case than some third party libraries will add additional contract IAsyncDisposableEx "with blackjack and hookers". Possibly it;s a reasonable solution to have IAsyncDisposableEx inherited from IAsyncDisposable with DisposedTask property, because "async using" implemented for IAsyncDisposable will also perfectly work with IAsyncDisposableEx. /// <summary>
/// Asynchronously disposable with additional features.
/// </summary>
public interface IAsyncDisposableEx: IAsyncDisposable
{
/// <summary>
/// Dispose task. Returns valid dispose task even <see cref="AsyncDispose"/> was not called yet. <br/>
/// The same task will be always returned from IAsyncDisposable.AsyncDispose() method (in the future calls).<br/>
/// When something goes wrong and problem cannot be handled - task should be completed with unprocessable exception to hint
/// application crash.
/// </summary>
Task DisposedTask { get; }
} |
Please also consider to support this scenario dotnet/csharplang#216 Possible way is: public interface ICancellableAsync: IDisposableAsync
{
void OnException (Exception ex);
} |
Such a feature could be great for inherently IO bound dispose like the Otherwise ressources which requires IO bound cleanup should all provide some |
The discussion here about using is a good example of one of the many thorny issues around how to evolve the c# language in view of the new async / await paradigm. Recently I opened a similar discussion about asynchrony and lock (which so far has not gained much traction). There was also a comment made elsewhere that iteration with await inside of yield can lead to similar issues as would occur if await were allowed inside of lock. Also if we look at the discussion above we can see a similar questions starting to arise for try / finally, such as how should finally deal with tasks that are being awaited in the try block, similar to the question of how using should deal with async operations. Overall, the control structures of c# were designed at a time when the normal programming paradigm was synchronous rather than asynchronous. And when asynchrony did occur, it was usually associated with threads rather than Tasks as we have today. It seems possible to me that we may need await versions of most of the major flow-of-control structures in the language. So we might have await using (...) {...} and await lock (...) {...} and try { ... } await catch (...) {...} await finally {...} and perhaps even await if (...) {...}. The question of how await should interact with foreach may be already answered by the C#8 proposal for async enumerables. It's easy for me to suggest, but I would like to call on @MadsTorgersen and the rest of the MS team to step back and give this some comprehensive thought (if it is not already being done). |
There is actually no need to asynchronously dispose and object. The state of an object can always be invalidated synchronously and cleanup can occur on the threadpool.
Why this works is a contract, that Dispose must not throw an exception. |
@mlehmk That only works if Dispose has no observable behavior. Pretend you implement that logic in FileStream that you've opened for exclusive access to a file, that you want to delete that immediately afterwards after disposing the FileStream. How would you know when you can delete the file? |
Comment: since this thread was opened a couple of years ago, I have been using (ha ha) the following interface: IAsyncShutdown { Task ShutdownAsync();}. I designed my program so that when the service provider scope exits, it will shut down and then dispose all the services. When I did this, I eventually found that I needed a ShuttingDown event in the IAsyncShutdown interface taking HandledEventArgs. The reason is that the program state may not always be suitable for the execution of the ShutdownAsync implemented by the service. For example, in some program states one may want to abandon an operation rather than complete it, or vice versa. |
This is a language design issue, so should be moved to csharplang. Since it is already tracked as part of the async-streams feature (candidate for C# 8.0, prototyped in async-streams branch), I'll go ahead and close the present issue. Thanks |
With the introduction of support for
await
inside of afinally
block, I'd like to propose the following extension to theusing
statement in C#.The
System.IAsyncDisposable
interfaceModification to the
using
statementWhen a
using
statement appears inside of anasync
method, the translation of theusing
statement is modified to the following.A
using
statement of the formcorresponds to one of four possible expansions. When
ResourceType
is a non-nullable value type which implementsSystem.IAsyncDisposable
, the expansion isOtherwise, when
ResourceType
is a non-nullable value type which does not implementSystem.IAsyncDisposable
, the expansion isOtherwise, when
ResourceType
is a nullable value type or a reference type other thandynamic
, the expansion is:Otherwise, when
ResourceType
isdynamic
, the expansion isThe
IAsyncDisposable
interface has no impact on the expansion ofusing
statements which appear in any context other than a method marked with theasync
modifier.The text was updated successfully, but these errors were encountered: