-
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
Proposal: Deprecate async void #13897
Comments
An analyzer, style guide or a warning wave might be a good way to discourage the use of said feature. Outright disabling it would be a breaking change. As well noted this form of |
This requires a change in the CLR. The _only_ reason There is actually a way around this limitation. but it requires writing your own event handling class and a bit of boilerplate code for each event. Backporting a pattern like this to existing libraries would also break just about every one of its consumers, but a brand new library could take advantage of it and provide async-safe events to its consumers. |
The CLR imposes no restrictions at all on the types of delegates that may be used as an event. Neither does C# (although VB.NET does). It is simply a convention that events return public delegate int Adder(int x, int y);
public class Calculator {
public event Adder Add;
}
var calc = new Calculator();
calc.Add += (x, y) => x + y; But that's a bit of a tangent as it's not really the responsibility of the event producer to know that the subscriber wants to do something asynchronous with the notification. |
I don't think any existing feature has ever been disabled in a future version of C# before, but this one might be worth it because IMO the drawbacks outweigh the benefits.
Perhaps a new task-like type similar to ValueTask can be introduced for event handlers. Event handlers can either return |
Well sure, but delegates in multicast form, rarely have an output only because there's no way to capture the actual results of the funcs in the invocation list. Func<int, Task<int>> one = async x => { await Task.Yield(); Console.WriteLine("called one"); return 1; };
. Func<int, Task<int>> two = async x => { await Task.Yield(); Console.WriteLine("called two"); return 2; };
. Func<int, Task<int>> three = one + two;
. await three.Invoke(3) Did it call both funcs? I have no idea. |
I think this is safe to do, Task DoSomethingAsync() { throw new Exception(); }
async void Interface.Foo() {
try { await DoSomethingAsync(); }
catch { Console.WriteLine("report exception"); } // will print
}
void DoFoo(Interface obj) {
try { obj.Foo(); }
catch { Console.WriteLine("caught"); } // won't print, of course
} Comes in handy when you want to implement interface void-returning methods in asynchronously and also take advantage of |
@alrz Yes, but you know the consequence of removing that try block. It's something many developers may not be aware of. |
The only way you'd be able to have the event return An analyzer can identify unsafe use of |
You can return a special purpose task-like |
Without querying its state how can it be The only safe way to bridge event handlers with async methods is through a pattern similar to that described by @alrz. I'd be fine if the compiler or analyzers would shove people not-so-gently in that direction. Perhaps even a warning wave could make it illegal to have an |
|
@benaadams Yes!
@HaloFour Dang! Yeah the internal state needs to be queried. Can the completion semantics of the proposed Task type mean: The event handler task has successfully invoked all delegates attached to the event but is not keeping track of asynchronous operations performed by those delegates? This way the Task is still fire and forget but can still be queried at some level. |
My understanding is that if there is a I'm definitely not arguing that using Queried by who? Without a continuation on that |
Yes, the behavior will be the same, however, there is an important difference. |
You can't change the return type without breaking the signature and precluding the use of that method as an event handler. In other words, it completely defeats the point. Developers would then have to use additional boilerplate to bridge event handlers to async methods manually. At the end of the day they'd be writing a lot more code for zero benefit, as the behavior would still be the same. Considering how obsessive the C# team has been about not introducing changes to how existing source builds, even to the point of not adding warnings to obviously errant code in more innocuous situations. They've never removed features from the compiler and rendered existing legal code illegal. The chances of them doing anything beyond a potential opt-in warning is unlikely. The fact that |
I agree that you shouldn't care about your subscribers callbacks. That said, I dislike async void, but I think there are things I'd much rather deprecate first like operator true |
Wow, never knew about that; might start using it 😉 |
I'm not introducing any new arguments. These are well known issues. However, a lot has changed since
This means there's a window of opportunity to fix a problematic feature. I understand the Microsoft mantra is to never break backwards compatibility but I think there is a path to fixing old warts without causing too much friction.
Yes, you can't change the return type without changing the method signature but maybe that's a good thing. It raises an important question: Is it beneficial for asynchronous event handlers to be distinctly identified, for example using a new EventHandlerAsync type? Just like async network operations are usually identified with an Async suffix? |
.NET Core was officially released in June. It's up to 1.0.1 now.
Which still doesn't help if there's nothing
No, this doesn't make any sense. The event itself is not asynchronous. The fact that the consumer wants to do something asynchronous is a completely separate concern. There's no reason for the producer to know nor care. |
.NET Core is still at the stage where .NET 1.0 was when it was initially released. Is anyone seriously using .NET Core in a public facing environment in production? Did you see the security advisory from a few days ago?
It helps because it moves an unsafe language level feature to the framework.
Not necessarily. What you're saying is correct for things like Winforms, where UI work is posted to the UI thread to process. The producer posts the work item and carries on. The rundown of this discussion so far is:
|
Officially supported? Yep.
You certainly would not be designing such a beast using C# events with or without
|
Breaking changes... slippery slope. Let's take the arguments mentioned above and apply them to "other areas"...
This is true of nearly every thing. But let's discuss goto for a second... there is no argument that nearly every scenario (especially common ones) that there is an alternative way to accomplish the task. So this, by this argument, would suggest that goto be removed. Right? However, what about all of the people that did use it because, for them, it was a solution that worked better for them and possibly even the only solution to a problem that they were encountering. Just because it's not something that you experience regularly doesn't mean there aren't scenarios out there where using something you "dislike" isn't the "right tool for the task at hand". So I'm going to call this one what it really is... "do it my way because I think my way is better". It's based on opinion, nothing more.
I'll comment that goto is got to be one of the most misunderstood tools out there. It's evil, never use it is the mantra... however, it's still alive and it's still kicking... why? Short answer... it's very useful for some very specific scenarios; one of which is simply raw performance. Even if that wasn't the case, some people think differently than others... so goto fits them better.
There is absolutely not argument from me that the use of goto can, if done incorrectly and non-judiciously, lead to disastrous results. The most common term for this result is "spaghetti code". No argument there. However, for the right circumstances... goto can be a lifesaver.
Ummm... seriously. "too advanced"? What does that even mean? Isn't using a language like C# "somewhat advanced"... there's a lot of things you have to know in order to accomplish the most simple tasks. The barrier of entry is FAR greater than say... oh I don't know... GW-BASIC and PRINT "HELLO WORLD!". Writing the same thing in C# requires more lines of code and, more interestingly enough, figuring out which freakin' kind of project you should create. Many people will get lost at that point. And speaking of goto... even though it's treated as the poster child of what not to do; the reality is that is HOW PROCESSORS WORK. Understanding of what is happening underneath/behind the higher abstraction of a language like C# is fundamental to understanding when/where/how goto would be of benefit. In other words... it's actually an ADVANCED topic that is periodically misused by people who don't understand what it's actually for. Going back to the original feature in question... this is also somewhat true of async void. It's a very advanced topic; however, it's been built in such a way that, for the most part, you don't need to know the details. Much like a goto, you can use it more generically without understating what is going on. With async void existing for primarily the purpose of not having to rewrite WinForms; and why should anyone... it works. And... for the most part, WinForms events should be "fire-and-forget". There are other frameworks that try to setup a rules-based approach to events; but that is an alternative tool. All of the above "arguments" are completely subjective. As such, none of them provide any reason to remove async void (IMO). The other thing to keep in mind... once something has been added... people get creative. Many times these usage scenarios were never foreseen. These features can take on a life of their own. Removing them after that is, IMO, more dangerous because it becomes nearly impossible to understand all of these other scenarios. And... at the end of the day... really... is it causing planes to fall from the skies? Adding new features to the language is hard enough (nameof for example); let's not complicate it further by trying to guess (and ultimately piss off the target audience) how removing something is going to "help". |
💭 We use an analyzer to turn |
There's no way I can do without this pattern for some view model properties: public int BoundProperty
{
get { return boundProperty; }
set
{
if (boundProperty == value) return;
boundProperty = value;
OnBoundPropertyChanged();
}
}
private async void OnBoundPropertyChanged()
{
// ...
} Not to mention the (rare) |
If you have to use async void like override async void M() { try { ... } catch { ... } } |
@alrz No way I'm swallowing exceptions like that in production code. That's worse than crashing. |
I didn't mean to just catch it. you should of course handle it, (log etc) and perhaps route it to UI thread, e.g. via dispatcher. |
I am agreeing with all reason the issuer posted. But I am thinking the opposite that we should drop Or maybe the opposite, we should drop async and let await be contextual whenever the return type is |
What is the "right" way to fire and forget a Task? In our app we did a helper: public static async void FireAndForget(this Task task)
{
try
{
await task.ConfigureAwait(false);
}
catch (Exception ex)
{
ErrorDialog.Show(ex); // helper class to show it to user
}
} Such a change would break our code. |
@molinch something like? public static void FireAndForget(this Task task)
{
task.ContinueWith(
t =>
{
ErrorDialog.Show(t.Exception); // helper class to show it to user
},
TaskContinuationOptions.OnlyOnFaulted);
}
} |
Forget this, I just did a couple of checks and yes it behaves normally. |
@benaadams We used your proposal, however in the end, we got an issue in our app, where a TaskCanceledException raised by a Promises task was never observed by the continuation. I can't explain why it happened but IMO that's a serious blocker for deprecating async void. |
Cancelled isn't faulted; change |
Thanks again ! Do you think that the special behaviour for OperationCanceledException is handled here? http://referencesource.microsoft.com/#mscorlib/system/threading/Tasks/Task.cs,2906 |
The fact that OperationCanceledException translates to cancellation rather than fault is an intrinsic part of the TPL API contract. If you need to work around this, you'll have to use |
Thanks @jnm2. Is there a chance to see which code, using sourceof.net, is responsible for it? I would be interested to understand it. |
This is what I'd do: public static void FireAndForget(this Task task)
{
task.ContinueWith(t =>
{
try
{
t.GetAwaiter().GetResult();
}
catch (Exception ex)
{
ErrorDialog.Show(ex); // helper class to show it to user
}
}, TaskContinuationOptions.NotOnRanToCompletion);
} |
@molinch relevant
|
@jnm2 that's probably a good idea, then will unwrap aggregate exceptions |
@molinch Looks like you've already found the code implementing the TPL contract. Here's the code for the |
Awesome, thanks both for your kind help |
When I get a spare moment, I'll be writing some final tests to get code merged to NUnit that moves the framework entirely to the awaitable contract away from the TPL contract because of things like these that have come up. |
We added this construct to the languages intentionally, fully aware of its limitations. I doubt we'd consider changing that now. |
If you would like to resurrect this discussion, please take it to |
I propose that the
async void
C# and VB language constructs be deprecated, for the following reasons.1. Its purpose can be achieved via other means
The code below
is more or less the same as
2. async void is widely misunderstood
Many tutorials on the web describe
async void
as a fire and forget feature but that's not completely accurate. async void methods can still block the caller if execution doesn't run into anawait
statement, and even then, it can still block the caller if the await call runs synchronously.Thus, async void methods have inconsistent return semantics. It can (a) return immediately -- while the rest of the method is processed by another task/thread, (b) block for a while and return -- while the rest of the method is processed by another task/thread, or (c) block indefinitely. The caller has no way of knowing what to expect.
In actuality,
async void
methods when factored with its weird exception handling behavior (discussed next) is really a fire and hope to forget feature.3. async void can be disastrous
If a developer unwittingly adds an
async
keyword to avoid
method, making the method anasync void
method. That developer has just added a time bomb to their application. Any exception that gets thrown in that method can take down the entire process. That exception cannot be caught by callers to theasync void
method.Also note that this applies to any exception thrown by methods the
async void
method calls.The code will still compile and run fine after the
void
toasync void
change and is small enough to easily slip through code review.4. async void is too advanced to be a language level feature
For anyone to use the
async void
feature safely, they have to be aware of its pitfalls and unique scenarios. It's the only language construct (that I'm aware of) that has a unique exception handling behavior, and that can stump even experienced developers.Removal of this construct will make the language a little more safer. Asynchronous event handlers can still be written using a helper as illustrated in [1].
The text was updated successfully, but these errors were encountered: