8

I would like to know the reasoning behind the way the compiler choose the TaskScheduler when compiling using the async keyword.

My test method is called by SignalR (ASP.NET host, IIS8, websocket transport) on the OnConnectedAsync method.

protected override async Task OnConnectedAsync(IRequest request, string connectionId)
{
   SendUpdates();
}

Starting a task on the Current synchronization context will result to an InvalidOperationException in System.Web.AspNetSynchronizationContext.OperationStarted()

An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %>.

Fine. With this SendUpdates definition, I get the above exception:

    private async void SendUpdates()
    {
        Task.Run(async () =>
            {
                while (true)
                {
                    await Task.Delay(1000);
                    await Connection.Broadcast("blabla");
                }
            });

    }

But even more interesting is when I don't get the exception. The following works:

    private void SendUpdates()

And the following works too

    private async Task SendUpdates()

this last one works too, but it's essentially the same as the above example.

    private Task SendUpdates()
    {
        return Task.Run(async () =>
            {
                while (true)
                {
                    await Task.Delay(1000);
                    await Connection.Broadcast("blabla");
                }
            });

    }

Do you know how the compiler choose which scheduler to use here?

svick
  • 236,525
  • 50
  • 385
  • 514
Eilistraee
  • 8,220
  • 1
  • 27
  • 30

2 Answers2

12

One of the primary guidelines in writing async code is "avoid async void" - that is, use async Task instead of async void unless you're implementing an async event handler.

async void methods use SynchronizationContext's OperationStarted and OperationCompleted; see my MSDN article It's All about the SynchronizationContext for more details.

ASP.NET detects the call to OperationStarted and (correctly) rejects it because it's illegal to put an async event handler there. When you correct the code to use async Task, then ASP.NET no longer sees an async event handler.

You may find my intro to async / await post helpful.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
3

When you call:

private async void SendUpdates()

With the call to Task.Run and using the async keyword on the anonymous delegate, you aren't actually providing a continuation; you start the Task, and you're giving the Run method a continuation, which it then processes. That continuation is not channeled back in any meaningful to the code that called Task.Run.

This is why you get the exception, the handler doesn't know to await on the Task that the call to Task.Run produces.

That said:

private void SendUpdates()

Works because the task is created and the code doesn't capture a SynchronizationContext (because there is no async keyword on the method, Task instances don't capture it by default). You are firing the task, but it's fire-and-forget.

And the following works too:

private async Task SendUpdates()

Namely because in returning the Task, you've returned an awaitable that the callback can work with.

To answer your question directly, the compiler will make sure to get the SynchronizationContext returned from SynchronizationContext.Current before you call await; whatever continuation is called after the awaitable returns will be called using that SynchronizationContext.

casperOne
  • 73,706
  • 19
  • 184
  • 253
  • In fact, I don't want to await anything. Do you mean that when I write async Task SendUpdates(), my inner task is wrapped in a Task compatible with the ASP.NET synchronization context, but that when I call async void SendUpdates(), this wrapping doesn't happen, which explains the issue? – Eilistraee Oct 23 '12 at 17:20
  • @Eilistraee When you write `async Task SendUpdates`, yes, the `SynchronizationContext` is captured and the `Task` that's returned is awaited on. When you have `async void SendUpdates`, you aren't calling await on anything. If anything, you should have at least a compiler warning indicating that you're using `async` but don't have a corresponding `await` (the `await` is on the anonymous delegate, not in the actual call code of the method). – casperOne Oct 23 '12 at 19:06
  • But even in the async Task SendUpdates() case, I am not awaiting the returned task. I mean, this task reference is lost, and the OnConnectedAsync continue its execution without awaiting anything (and its a good thing as the inner task won't complete) – Eilistraee Oct 23 '12 at 20:26
  • @Eilistraee You don't have to await the returned task, the returned task is awaited upon, whereas in the first case, there's nothing to be awaited upon, but a new task is started regardless. – casperOne Oct 23 '12 at 20:55