Skip to content
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: async blocks #10503

Closed
alrz opened this issue Apr 12, 2016 · 11 comments
Closed

Proposal: async blocks #10503

alrz opened this issue Apr 12, 2016 · 11 comments

Comments

@alrz
Copy link
Member

alrz commented Apr 12, 2016

Currently, to use asynchronous features, you need to change the method signature to return a Task, but in some cases it would not be possible to do so, e.g. implementing an interface, writing event handlers or the entry point. In these cases developers often end up with highly discouraged async void, ContinueWith calls, or manually created bridges which (to quote from #7476) "is a waste of time and can be a cause of bugs."

It is proposed (#7476) to make this a special case for entry points but it's not the only place that prevents you to use async/await and still there are chances that you will need to write these bridges or consider any other workarounds.

It would be nice to be able to write an async block, which enables us to use await without changing the method signature and still don't lose readability of the code.

void IActionInvoker.Execute(MethodInfo method) {
  var task = method.Invoke(null) as Task;
  if (task != null) {
    ShowIndicator();
    task.ContinueWith(t => {
      HideIndicator();
    }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously | 
      TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default);
    task.ContinueWith(t => {
      HideIndicator();
      ShowError(t.Exception);
    }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously | 
      TaskContinuationOptions.OnlyOnFaulted, TaskScheduler.Default);
  }
}

void IActionInvoker.Execute(MethodInfo method) {
  var task = method.Invoke(null) as Task;
  if (task != null) {
    ShowIndicator();
    async {
      try { await task; }
      catch(Exception ex) { ShowError(ex); }
      finally { HideIndicator(); }
    } 
  }
}

This is not the exact translation but something similar should work for the Main method or in other contexts.

@HaloFour
Copy link

@alrz You can mark void methods as async. The reasons why it is considered bad practice would apply to these async blocks as well, you have no mechanism to propagate the result back to the caller since the "async" portion may continue after the method has returned.

I think that #7169 would be more apt to solve issues like these, however since you'd still need a task-like analog you still can't really make a synchronous method signatures asynchronous (while expecting to return the result to the caller.)

Also, wouldn't local functions be sufficient in declaring nested "blocks" of asynchronous code that could could invoke from within a non-async method?

void IActionInvoker.Execute(MethodInfo method) {
    var task = method.Invoke(null) as Task;
    UpdateIndicator();

    async void UpdateIndicator() {
        ShowIndicator();
        try { await task; }
        catch(Exception ex) { ShowError(ex); }
        finally { HideIndicator(); }
    }
}

@alrz
Copy link
Member Author

alrz commented Apr 12, 2016

You can mark void methods as async. The reasons why it is considered bad practice would apply to these async blocks as well, you have no mechanism to propagate the result back to the caller since the "async" portion may continue after the method has returned.

You can use return anywhere in an async block, it'll get blocked until await returns and then it gets back to the caller. If you're doing this in a synchronous method, this is probably what you want. And if that was not the case you will need ContinueWith calls which I think await is meant to avoid. async blocks just help to reduce noises around Task API in any of these cases. To not lose exceptions, I can think of this to be added to try so when you use async try you will have to catch exceptions anyway.

wouldn't local functions be sufficient in declaring nested "blocks" of asynchronous code that could could invoke from within a non-async method?

This would apply to Main methods as well, but it loses the point of being direct when you're writing these kind of code. But yes, it would be more like an immediately invoked anonymous async local function but it wouldn't be the same. I think it'd be unfortunate to go to all the trouble just for the sake of Main method.

@HaloFour
Copy link

@alrz Making entry points asynchronous is a special case because it is practically the only case where blocking on an asynchronous call is considered safe. Elsewhere you are inviting deadlocks since you cannot know if asynchronous methods called depends on a synchronization context. Having language support for this would encourage a very bad practice.

@gafter
Copy link
Member

gafter commented Apr 17, 2016

@alrz You describe the syntax you want to write, but not its semantics. What would it do?

@alrz
Copy link
Member Author

alrz commented Apr 17, 2016

@gafter The idea is that it would get blocked if you return from an async block i.e. turning every await to GetAwaiter().GetResult(), and otherwise it should fire-and-forget the async part. In the latter case the compiler could produce a warning if exceptions are not caught.

@gafter
Copy link
Member

gafter commented Apr 17, 2016

@gafter The idea is that it would get blocked if you return from an async block i.e. turning every await to GetAwaiter().GetResult()

In other words, an async block would not be asynchronous at all, but blocking. Doesn't sound very async to me.

@alrz
Copy link
Member Author

alrz commented Apr 17, 2016

@gafter Same would apply to async Main, the point is to be able to use await instead of falling back to Task API in a syncrounous method. Yes it just feels like async but when you are returning from an asyncrounous part of a syncrounous method, what do you expect? On the other hand, if you did not use return in an async block it executes asynchronously (fire-and-forget) and if you catch exceptions appropriately, there's nothing left to worry about.

The difference of this and async computations in F# is that they are expressions and they have a type. Still, you cannot interact with the resultant value without leaving the monad, i.e. waiting for it to complete, alternatively you can Start it and pass it to ignore which is what we're doing here.

@svick
Copy link
Contributor

svick commented Apr 17, 2016

@alrz

On the other hand, if you did not use return in an async block it executes asynchronously (fire-and-forget)

That sounds very unintuitive to me. In a void method, adding return; at the end does nothing. But in an async block, it would completely change what it does?

@alrz
Copy link
Member Author

alrz commented Apr 17, 2016

@svick Not always, it might change the type of the enclosing lambda (#7681) 😄

@HaloFour
Copy link

@alrz

Both of the described behaviors are very much considered bad practices in asynchronous code. The entry point is the one exception. As such I don't think that a general purpose mechanism should be introduced into the language as it would encourage people to use it outside of Main where they absolutely should not be using it.

@alrz
Copy link
Member Author

alrz commented Apr 18, 2016

Fair enough.

@alrz alrz closed this as completed Apr 18, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants