为啥 Thread.CurrentPrincipal 需要“等待 Task.Yield()”才能正确流动?

Posted

技术标签:

【中文标题】为啥 Thread.CurrentPrincipal 需要“等待 Task.Yield()”才能正确流动?【英文标题】:Why is an "await Task.Yield()" required for Thread.CurrentPrincipal to flow correctly?为什么 Thread.CurrentPrincipal 需要“等待 Task.Yield()”才能正确流动? 【发布时间】:2013-05-15 05:37:59 【问题描述】:

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

我正在尝试以异步方法同时分配 HttpContext.Current.UserThread.CurrentPrincipalThread.CurrentPrincipal 的分配不正确,除非执行 await Task.Yield();(或其他任何异步操作)(将 true 传递给 AuthenticateAsync() 将导致成功)。

这是为什么呢?

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[] )
            
            
        
    

注意事项:

await Task.Yield(); 可以出现在AuthenticateAsync() 中的任何位置,也可以在调用AuthenticateAsync() 后移动到GetAsync() 中,它仍然会成功。 ApiController.User 返回Thread.CurrentPrincipalHttpContext.Current.User 始终正确流动,即使没有 await Task.Yield()Web.config 包括&lt;httpRuntime targetFramework="4.5"/&gt; 其中implies UseTaskFriendlySynchronizationContext。 几天前我问过a similar question,但没有意识到这个例子之所以成功,是因为Task.Delay(1000) 存在。

【问题讨论】:

如果你忽略它会发生什么? @SLaks,如果 await Task.Yield() 被跳过,Thread.CurrentPrincipal 将恢复到调用 await AuthenticateAsync() 之前的状态。由于Thread.CurrentPrincipal 不再是MyPrincipal,因此抛出异常。 在我的 Owin 中间件中,我必须链接最后一个中间件,它只是等待 Task.Yield();这似乎会导致 Thread.CurrentPrincipal 在整个执行过程中按预期运行。 【参考方案1】:

多么有趣! Thread.CurrentPrincipal 似乎是基于 logical 调用上下文,而不是每个线程调用上下文。 IMO 这很不直观,我很想知道为什么要以这种方式实施。


在 .NET 4.5. 中,async 方法与逻辑调用上下文交互,因此它可以更正确地与async 方法一起使用。我有一个blog post on the topic; AFAIK 这是唯一记录它的地方。在 .NET 4.5 中,在每个 async 方法的开头,它会为其逻辑调用上下文激活“写时复制”行为。当(如果)逻辑调用上下文被修改时,它将首先创建自己的本地副本。

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

如果您没有Yield,那么当您设置Thread.CurrentPrincipal 时,您将创建逻辑调用上下文的副本,该副本被视为该async 方法的“本地”。当async 方法返回时,该本地上下文将被丢弃并取而代之的是原始上下文(您可以看到ExecutionContextBelongsToCurrentScope 返回到false)。

另一方面,如果您执行Yield,则SynchronizationContext 行为会接管。实际发生的是HttpContext 被捕获并用于恢复这两种方法。在这种情况下,您没有看到 Thread.CurrentPrincipalAuthenticateAsync 保留到 GetAsync;在方法恢复之前实际发生的是HttpContext is preserved, and then HttpContext.User is overwriting Thread.CurrentPrincipal

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

【讨论】:

嗨!您是否听说过为什么要以这种方式实施?这篇文章我已经读了 3 遍了,仍然让我大吃一惊。 @vtortola:我不确定。我认为这是为了让用户权限自动流向后台线程。这可能在十年前就已经完成了,并且更新时复制上下文的行为要更新得多。所以他们以这种奇怪的方式发生冲突。

以上是关于为啥 Thread.CurrentPrincipal 需要“等待 Task.Yield()”才能正确流动?的主要内容,如果未能解决你的问题,请参考以下文章

你应该同步运行方法吗?为啥或者为啥不?

为啥使用 glTranslatef?为啥不直接更改渲染坐标?

为啥 DataGridView 上的 DoubleBuffered 属性默认为 false,为啥它受到保护?

为啥需要softmax函数?为啥不简单归一化?

为啥 g++ 需要 libstdc++.a?为啥不是默认值?

为啥或为啥不在 C++ 中使用 memset? [关闭]