Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is an "await Task.Yield()" required for Thread.CurrentPrincipal to flow correctly?

The code below was added to a freshly created Visual Studio 2012 .NET 4.5 WebAPI project.

I'm trying to assign both HttpContext.Current.User and Thread.CurrentPrincipal in an asynchronous method. The assignment of Thread.CurrentPrincipal flows incorrectly unless an await Task.Yield(); (or anything else asynchronous) is executed (passing true to AuthenticateAsync() will result in success).

Why is that?

using System.Security.Principal; using System.Threading.Tasks; using System.Web.Http;  namespace ExampleWebApi.Controllers {     public class ValuesController : ApiController     {         public async Task GetAsync()         {             await AuthenticateAsync(false);              if (!(User is MyPrincipal))             {                 throw new System.Exception("User is incorrect type.");             }         }          private static async Task AuthenticateAsync(bool yield)         {             if (yield)             {                 // Why is this required?                 await Task.Yield();             }              var principal = new MyPrincipal();             System.Web.HttpContext.Current.User = principal;             System.Threading.Thread.CurrentPrincipal = principal;         }          class MyPrincipal : GenericPrincipal         {             public MyPrincipal()                 : base(new GenericIdentity("<name>"), new string[] {})             {             }         }     } } 

Notes:

  • The await Task.Yield(); can appear anywhere in AuthenticateAsync() or it can be moved into GetAsync() after the call to AuthenticateAsync() and it will still succeed.
  • ApiController.User returns Thread.CurrentPrincipal.
  • HttpContext.Current.User always flows correctly, even without await Task.Yield().
  • Web.config includes <httpRuntime targetFramework="4.5"/> which implies UseTaskFriendlySynchronizationContext.
  • I asked a similar question a couple days ago, but did not realize that example was only succeeding because Task.Delay(1000) was present.
like image 401
Jon-Eric Avatar asked May 20 '13 15:05

Jon-Eric


People also ask

What does await task Yield do?

You can use await Task. Yield(); in an asynchronous method to force the method to complete asynchronously. If there is a current synchronization context (SynchronizationContext object), this will post the remainder of the method's execution back to that context.

Does await task delay block thread?

await Task. Delay(1000) doesn't block the thread, unlike Task. Delay(1000).

Does await free the thread?

The await keyword, by contrast, is non-blocking, which means the current thread is free to do other things during the wait.

How does task Wait work?

Wait is a synchronization method that causes the calling thread to wait until the current task has completed. If the current task has not started execution, the Wait method attempts to remove the task from the scheduler and execute it inline on the current thread.


1 Answers

How interesting! It appears that Thread.CurrentPrincipal is based on the logical call context, not the per-thread call context. IMO this is quite unintuitive and I'd be curious to hear why it was implemented this way.


In .NET 4.5., async methods interact with the logical call context so that it will more properly flow with async methods. I have a blog post on the topic; AFAIK that's the only place where it's documented. In .NET 4.5, at the beginning of every async method, it activates a "copy-on-write" behavior for its logical call context. When (if) the logical call context is modified, it will create a local copy of itself first.

You can see the "localness" of the logical call context (i.e., whether it has been copied) by observing System.Threading.Thread.CurrentThread.ExecutionContextBelongsToCurrentScope in a watch window.

If you don't Yield, then when you set Thread.CurrentPrincipal, you're creating a copy of the logical call context, which is treated as "local" to that async method. When the async method returns, that local context is discarded and the original context takes its place (you can see ExecutionContextBelongsToCurrentScope returning to false).

On the other hand, if you do Yield, then the SynchronizationContext behavior takes over. What actually happens is that the HttpContext is captured and used to resume both methods. In this case, you're not seeing Thread.CurrentPrincipal preserved from AuthenticateAsync to GetAsync; what is actually happening is HttpContext is preserved, and then HttpContext.User is overwriting Thread.CurrentPrincipal before the methods resume.

If you move the Yield into GetAsync, you see similar behavior: Thread.CurrentPrincipal is treated as a local modification scoped to AuthenticateAsync; it reverts its value when that method returns. However, HttpContext.User is still set correctly, and that value will be captured by Yield and when the method resumes, it will overwrite Thread.CurrentPrincipal.

like image 143
Stephen Cleary Avatar answered Oct 13 '22 05:10

Stephen Cleary