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?
One of the primary guidelines in writing
async
code is "avoidasync void
" - that is, useasync Task
instead ofasync void
unless you're implementing anasync
event handler.async void
methods useSynchronizationContext
'sOperationStarted
andOperationCompleted
; 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 anasync
event handler there. When you correct the code to useasync Task
, then ASP.NET no longer sees anasync
event handler.You may find my intro to
async
/await
post helpful.When you call:
With the call to
Task.Run
and using theasync
keyword on the anonymous delegate, you aren't actually providing a continuation; you start theTask
, and you're giving theRun
method a continuation, which it then processes. That continuation is not channeled back in any meaningful to the code that calledTask.Run
.This is why you get the exception, the handler doesn't know to
await
on theTask
that the call toTask.Run
produces.That said:
Works because the task is created and the code doesn't capture a
SynchronizationContext
(because there is noasync
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:
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 fromSynchronizationContext.Current
before you callawait
; whatever continuation is called after the awaitable returns will be called using thatSynchronizationContext
.