为什么Thread.CurrentPrincipal需要"await Task.Yield()"才能正确流动?

Jon*_*ric 37 c# async-await asp.net-web-api

下面的代码已添加到新创建的Visual Studio 2012 .NET 4.5 WebAPI项目中.

我正在尝试分配两者HttpContext.Current.UserThread.CurrentPrincipal在异步方法中.Thread.CurrentPrincipal流量分配不正确,除非执行await Task.Yield();(或其他任何异步)(传递trueAuthenticateAsync()将导致成功).

这是为什么?

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[] {})
            {
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

笔记:

  • await Task.Yield();可以出现在任何地方AuthenticateAsync(),或者可以移动到GetAsync()调用后AuthenticateAsync(),它仍然会成功.
  • ApiController.User回报Thread.CurrentPrincipal.
  • HttpContext.Current.User总是正确地流动,即使没有await Task.Yield().
  • Web.config包括<httpRuntime targetFramework="4.5"/>暗示 UseTaskFriendlySynchronizationContext.
  • 几天前我问了一个类似的问题,但没有意识到这个例子只是因为Task.Delay(1000)存在而成功.

Ste*_*ary 41

很有意思!它似乎Thread.CurrentPrincipal基于逻辑调用上下文,而不是每线程调用上下文.IMO这是非常不直观的,我很想知道它为什么以这种方式实现.


在.NET 4.5.中,async方法与逻辑调用上下文交互,以便它更适合于使用async方法.我有关于这个主题博客文章 ; AFAIK是唯一记录它的地方.在.NET 4.5中,在每个async方法的开头,它为其逻辑调用上下文激活"写时复制"行为.当(如果)修改逻辑调用上下文时,它将首先创建自身的本地副本.

您可以通过System.Threading.Thread.CurrentThread.ExecutionContextBelongsToCurrentScope在监视窗口中观察来查看逻辑调用上下文的"本地性"(即,是否已复制).

如果不这样做Yield,那么在设置时Thread.CurrentPrincipal,您将创建逻辑调用上下文的副本,该副本被视为该async方法的"本地" .当async方法返回时,将丢弃该本地上下文并且原始上下文取代它(您可以看到ExecutionContextBelongsToCurrentScope返回false).

另一方面,如果你这样做Yield,那么SynchronizationContext行为就会接管.实际发生的HttpContext是捕获并用于恢复两种方法.在这种情况下,你没有看到Thread.CurrentPrincipal保存AuthenticateAsyncGetAsync; 实际发生的HttpContext是保留,然后HttpContext.UserThread.CurrentPrincipal在方法恢复之前覆盖.

如果你移动YieldGetAsync,你会看到类似的行为:Thread.CurrentPrincipal被视为作为范围的本地修改AuthenticateAsync; 它在该方法返回时恢复其值.但是,HttpContext.User仍然设置正确,并且该值将被捕获,Yield并且当方法恢复时,它将覆盖Thread.CurrentPrincipal.

  • 你好!您听说过为什么要这样实施吗?我已经读了这篇文章 3 遍了,但它仍然让我震惊。 (2认同)
  • @vtortola:我不确定.我认为这是因为用户权限会自动流向后台线程.这可能是十年前做的,并且更新后的复制上下文行为更新近.所以他们以这种奇怪的方式发生冲突. (2认同)