c# - AspNetSynchronizationContext and await continuations in ASP.NET -


i noticed unexpected (and i'd say, redundant) thread switch after await inside asynchronous asp.net web api controller method.

for example, below i'd expect see same managedthreadid @ locations #2 , 3#, see different thread @ #3:

public class testcontroller : apicontroller {     public async task<string> getdata()     {         debug.writeline(new         {             = "1) before await",             thread = thread.currentthread.managedthreadid,             context = synchronizationcontext.current         });          await task.delay(100).continuewith(t =>         {             debug.writeline(new             {                 = "2) inside continuewith",                 thread = thread.currentthread.managedthreadid,                 context = synchronizationcontext.current             });         }, taskcontinuationoptions.executesynchronously); //.configureawait(false);          debug.writeline(new         {             = "3) after await",             thread = thread.currentthread.managedthreadid,             context = synchronizationcontext.current         });          return "ok";     } } 

i've looked @ implementation of aspnetsynchronizationcontext.post, comes down this:

task newtask = _lastscheduledtask.continuewith(_ => safewrapcallback(action)); _lastscheduledtask = newtask; 

thus, the continuation scheduled on threadpool, rather gets inlined. here, continuewith uses taskscheduler.current, in experience instance of threadpooltaskscheduler inside asp.net (but doesn't have that, see below).

i eliminate redundant thread switch configureawait(false) or custom awaiter, take away automatic flow of http request's state properties httpcontext.current.

there's side effect of current implementation of aspnetsynchronizationcontext.post. it results in deadlock in 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 bit contrived, shows may happen if taskscheduler.current taskscheduler.fromcurrentsynchronizationcontext(), i.e., made aspnetsynchronizationcontext. doesn't use blocking code , have been executed smoothly in winforms or wpf.

this behavior of aspnetsynchronizationcontext different v4.0 implementation (which still there legacyaspnetsynchronizationcontext).

so, reason such change? thought, idea behind might reduce gap deadlocks, deadlock still possible current implementation, when using task.wait() or task.result.

imo, it'd more appropriate put this:

task newtask = _lastscheduledtask.continuewith(_ => safewrapcallback(action),     taskcontinuationoptions.executesynchronously); _lastscheduledtask = newtask; 

or, @ least, i'd expect use taskscheduler.default rather taskscheduler.current.

if enable legacyaspnetsynchronizationcontext <add key="aspnet:usetaskfriendlysynchronizationcontext" value="false" /> in web.config, works desired: synchronization context gets installed on thread awaited task has ended, , continuation synchronously executed there.

that continuation being dispatched onto new thread rather inlined intentional. let's break down:

  1. you're calling task.delay(100). after 100 milliseconds, underlying task transition completed state. transition happen on arbitrary threadpool / iocp thread; won't happen on thread under asp.net sync context.

  2. the .continuewith(..., executesynchronously) cause debug.writeline(2) take place on thread transitioned task.delay(100) terminal state. continuewith return new task.

  3. you're awaiting task returned [2]. since thread completes task [2] isn't under control of asp.net sync context, async / await machinery call synchronizationcontext.post. method contracted always dispatch asynchronously.

the async / await machinery have optimizations execute continuations inline on completing thread rather calling synchronizationcontext.post, optimization kicks in if completing thread running under sync context it's dispatch to. isn't case in sample above, [2] running on arbitrary thread pool thread, needs dispatch aspnetsynchronizationcontext run [3] continuation. explains why thread hop doesn't occur if use .configureawait(false): [3] continuation can inlined in [2] since it's going dispatched under default sync context.

to other questions re: task.wait() , task.result, new sync context not intended reduce deadlock conditions relative .net 4.0. (in fact, it's easier deadlocks in new sync context in old context.) new sync context intended have implementation of .post() plays async / await machinery, old sync context failed miserably @ doing. (the old sync context's implementation of .post() block calling thread until synchronization primitive available, dispatch callback inline.)

calling task.wait() , task.result request thread on task not known completed can still cause deadlocks, calling task.wait() or task.result ui thread in win forms or wpf application.

finally, weirdness task.factory.startnew might actual bug. until there's actual (non-contrived) scenario support this, team not inclined investigate further.


Comments

Popular posts from this blog

apache - Remove .php and add trailing slash in url using htaccess not loading css -

javascript - jQuery show full size image on click -