Organizational Research By

Surprising Reserch Topic

aspnetsynchronizationcontext and await continuations in asp net


aspnetsynchronizationcontext and await continuations in asp net  using -'c#,asp.net,.net,task-parallel-library,async-await'

I noticed an unexpected (and I'd say, a redundant) thread switch after await inside asynchronous ASP.NET Web API controller method.

For example, below I'd expect to see the same ManagedThreadId at locations #2 and 3#, but most often I see a different thread at #3:

public class TestController : ApiController
{
    public async Task<string> GetData()
    {
        Debug.WriteLine(new
        {
            where = "1) before await",
            thread = Thread.CurrentThread.ManagedThreadId,
            context = SynchronizationContext.Current
        });

        await Task.Delay(100).ContinueWith(t =>
        {
            Debug.WriteLine(new
            {
                where = "2) inside ContinueWith",
                thread = Thread.CurrentThread.ManagedThreadId,
                context = SynchronizationContext.Current
            });
        }, TaskContinuationOptions.ExecuteSynchronously); //.ConfigureAwait(false);

        Debug.WriteLine(new
        {
            where = "3) after await",
            thread = Thread.CurrentThread.ManagedThreadId,
            context = SynchronizationContext.Current
        });

        return "OK";
    }
}


I've looked at the implementation of AspNetSynchronizationContext.Post, essentially it comes down to this:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;


Thus, the continuation is scheduled on ThreadPool, rather than gets inlined. Here, ContinueWith uses TaskScheduler.Current, which in my experience is always an instance of ThreadPoolTaskScheduler inside ASP.NET (but it doesn't have to be that, see below).

I could eliminate a redundant thread switch like this with ConfigureAwait(false) or a custom awaiter, but that would take away the automatic flow of the HTTP request's state properties like HttpContext.Current.

There's another side effect of the current implementation of AspNetSynchronizationContext.Post. It results in a deadlock in the following case:

await Task.Factory.StartNew(
    async () =>
    {
        return await Task.Factory.StartNew(
            () => Type.Missing,
            CancellationToken.None,
            TaskCreationOptions.None,
            scheduler: TaskScheduler.FromCurrentSynchronizationContext());
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    scheduler: TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();


This example, albeit a bit contrived, shows what may happen if TaskScheduler.Current is TaskScheduler.FromCurrentSynchronizationContext(), i.e., made from AspNetSynchronizationContext. It doesn't use any blocking code and would have been executed smoothly in WinForms or WPF.

This behavior of AspNetSynchronizationContext is different from the v4.0 implementation (which is still there as LegacyAspNetSynchronizationContext).

So, what is the reason for such change? I thought, the idea behind this might be to reduce the gap for deadlocks, but deadlock are still possible with the current implementation, when using Task.Wait() or Task.Result.

IMO, it'd more appropriate to put it like this:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),
    TaskContinuationOptions.ExecuteSynchronously);
_lastScheduledTask = newTask;


Or, at least, I'd expect it to use TaskScheduler.Default rather than TaskScheduler.Current.

If I enable LegacyAspNetSynchronizationContext with <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" /> in web.config, it works as desired: the synchronization context gets installed on the thread where the awaited task has ended, and the continuation is synchronously executed there.
    
asked Sep 24, 2015 by badhwar.rohit
0 votes
15 views



Related Hot Questions



Government Jobs Opening


...