【问题标题】:Setting Thread.CurrentPrincipal with async/await使用 async/await 设置 Thread.CurrentPrincipal
【发布时间】:2015-09-29 13:05:35
【问题描述】:

下面是我尝试在异步方法中将 Thread.CurrentPrincipal 设置为自定义 UserPrincipal 对象的简化版本,但自定义对象在离开等待后会丢失,即使它仍在新的 threadID 10 上。

有没有办法在等待中更改 Thread.CurrentPrincipal 并稍后使用它而不传递或返回它?或者这是不安全的,不应该是异步的?我知道有线程更改,但认为 async/await 会为我处理同步。

[TestMethod]
public async Task AsyncTest()
{
    var principalType = Thread.CurrentPrincipal.GetType().Name;
    // principalType = WindowsPrincipal
    // Thread.CurrentThread.ManagedThreadId = 11

    await Task.Run(() =>
    {
        // Tried putting await Task.Yield() here but didn't help

        Thread.CurrentPrincipal = new UserPrincipal(Thread.CurrentPrincipal.Identity);
        principalType = Thread.CurrentPrincipal.GetType().Name;
        // principalType = UserPrincipal
        // Thread.CurrentThread.ManagedThreadId = 10
    });
    principalType = Thread.CurrentPrincipal.GetType().Name;
    // principalType = WindowsPrincipal (WHY??)
    // Thread.CurrentThread.ManagedThreadId = 10
}

【问题讨论】:

  • 您是否在使用 ASP.NET,如果是,您可能想切换到 HttpContext.Current.User,这将为您提供您正在寻找的行为。
  • 呵呵,我认为这会出现,并且应该指定这必须适用于 HttpContext 不可用的非 Web 应用程序。
  • 我没想到你在哪里(你很少想在网络应用程序中做Task.Run)。

标签: c# asynchronous async-await claims-based-identity current-principal


【解决方案1】:

我知道有线程更改,但我认为 async/await 会为我处理同步。

async/await 本身不会同步任何线程本地数据。不过,如果您想自己进行同步,它确实有某种“钩子”。

默认情况下,当您await 一个任务时,它将捕获当前的“上下文”(即SynchronizationContext.Current,除非它是null,在这种情况下它是TaskScheduler.Current)。当async 方法恢复时,它将在该上下文中恢复。

因此,如果您想定义“上下文”,您可以通过定义自己的SynchronizationContext 来实现。不过,这并不容易。特别是如果您的应用程序需要在 ASP.NET 上运行,这需要它自己的 AspNetSynchronizationContext(并且它们不能嵌套或任何东西 - 你只能得到一个)。 ASP.NET 使用其SynchronizationContext 来设置Thread.CurrentPrincipal

但是,请注意,SynchronizationContext 有明确的移动远离。 ASP.NET vNext 没有。 OWIN 从来没有这样做过(AFAIK)。自托管 SignalR 也没有。通常认为以 some 的方式传递值更合适 - 无论这是对方法明确的,还是注入到包含该方法的类型的成员变量中。

如果您真的不想传递该值,那么您也可以采用另一种方法:async-等价于ThreadLocal。核心思想是将不可变的值存储在LogicalCallContext 中,它被异步方法适当地继承。我在我的博客上介绍了这个"AsyncLocal"(有传言称AsyncLocal 可能会出现在.NET 4.6 中,但在那之前你必须自己动手)。请注意,您无法使用AsyncLocal 技术读取Thread.CurrentPrincipal;您必须更改所有代码才能使用 MyAsyncValues.CurrentPrincipal 之类的内容。

【讨论】:

  • AsyncLocal in 4.6 不是谣言,it's there 没有警告说此功能可能会在发布前被删除。
  • 感谢斯蒂芬的快速回复!我希望这会引起您的注意:-) 在特定情况下处理对象时这是真的还是只是 Thread.CurrentPrincipal 的特例?例如,我可以有一个属于 Cat 子类型的 Animal,然后在等待的 Task 中将其更改为子类型 Dog,并且在等待之后它仍然是 Dog。
  • @StephenCleary 关于 AsyncLocal 的好文章,但我认为它不适用于他的场景。 LogicalCallContext 将以一种方式从调用者流向被调用者,如果被调用者添加或更改一个值,它不会流回调用者对吗?
  • @JeffCyr:没错。异步本地仅从调用者流向被调用者,因此操作必须在调用之前设置值。
  • @Noseratio:好点;在这种情况下,AsyncLocal 不会比 CurrentPrincipal 已经提供的任何好处。
【解决方案2】:

Thread.CurrentPrincipal 存储在 ExecutionContext 中,ExecutionContext 存储在 Thread Local Storage 中。

在另一个线程(使用 Task.Run 或 ThreadPool.QueueWorkItem)上执行委托时,会从当前线程捕获 ExecutionContext,并将委托包装在 ExecutionContext.Run 中。因此,如果在调用 Task.Run 之前设置 CurrentPrincipal,它仍然会在 Delegate 中设置。

现在您的问题是您更改了 Task.Run 中的 CurrentPrincipal 并且 ExecutionContext 仅以一种方式流动。我认为这是大多数情况下的预期行为,解决方案是在开始时设置 CurrentPrincipal。

在任务中更改 ExecutionContext 时,您最初想要的内容是不可能的,因为 Task.ContinueWith 也捕获了 ExecutionContext。要做到这一点,您必须在 Delegate 运行后立即以某种方式捕获 ExecutionContext,然后在自定义等待者的延续中将其流回,但这将是非常邪恶的。

【讨论】:

  • 感谢 Jeff 解释流程。我正在讨论自定义路线,但正如你所说,这很可怕。
【解决方案3】:

您可以使用自定义等待程序来处理CurrentPrincipal(或任何线程属性)。下面的例子展示了它是如何完成的,灵感来自Stephen Toub's CultureAwaiter。它在内部使用TaskAwaiter,因此也会捕获同步上下文(如果有)。

用法:

Console.WriteLine(Thread.CurrentPrincipal.GetType().Name);

await TaskExt.RunAndFlowPrincipal(() => 
{
    Thread.CurrentPrincipal = new UserPrincipal(Thread.CurrentPrincipal.Identity);
    Console.WriteLine(Thread.CurrentPrincipal.GetType().Name);
    return 42;
});

Console.WriteLine(Thread.CurrentPrincipal.GetType().Name);

代码(仅经过少量测试):

public static class TaskExt
{
    // flowing Thread.CurrentPrincipal
    public static FlowingAwaitable<TResult, IPrincipal> RunAndFlowPrincipal<TResult>(
        Func<TResult> func,
        CancellationToken token = default(CancellationToken))
    {
        return RunAndFlow(
            func,
            () => Thread.CurrentPrincipal, 
            s => Thread.CurrentPrincipal = s,
            token);
    }

    // flowing anything
    public static FlowingAwaitable<TResult, TState> RunAndFlow<TResult, TState>(
        Func<TResult> func,
        Func<TState> saveState, 
        Action<TState> restoreState,
        CancellationToken token = default(CancellationToken))
    {
        // wrap func with func2 to capture and propagate exceptions
        Func<Tuple<Func<TResult>, TState>> func2 = () =>
        {
            Func<TResult> getResult;
            try
            {
                var result = func();
                getResult = () => result;
            }
            catch (Exception ex)
            {
                // capture the exception
                var edi = ExceptionDispatchInfo.Capture(ex);
                getResult = () => 
                {
                    // re-throw the captured exception 
                    edi.Throw(); 
                    // should never be reaching this point, 
                    // but without it the compiler whats us to 
                    // return a dummy TResult value here
                    throw new AggregateException(edi.SourceException);
                }; 
            }
            return new Tuple<Func<TResult>, TState>(getResult, saveState());    
        };

        return new FlowingAwaitable<TResult, TState>(
            Task.Run(func2, token), 
            restoreState);
    }

    public class FlowingAwaitable<TResult, TState> :
        ICriticalNotifyCompletion
    {
        readonly TaskAwaiter<Tuple<Func<TResult>, TState>> _awaiter;
        readonly Action<TState> _restoreState;

        public FlowingAwaitable(
            Task<Tuple<Func<TResult>, TState>> task, 
            Action<TState> restoreState)
        {
            _awaiter = task.GetAwaiter();
            _restoreState = restoreState;
        }

        public FlowingAwaitable<TResult, TState> GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _awaiter.IsCompleted; }
        }

        public TResult GetResult()
        {
            var result = _awaiter.GetResult();
            _restoreState(result.Item2);
            return result.Item1();
        }

        public void OnCompleted(Action continuation)
        {
            _awaiter.OnCompleted(continuation);
        }

        public void UnsafeOnCompleted(Action continuation)
        {
            _awaiter.UnsafeOnCompleted(continuation);
        }
    }
}

【讨论】:

  • 我认为这是完成这项工作的最佳机会,所以我会看看我能用它做什么。感谢您抽出宝贵时间整理!
【解决方案4】:

ExecutionContext,包含SecurityContext,包含CurrentPrincipal,几乎总是流经所有异步分叉。因此,在您的 Task.Run() 委托中,您 - 在您注意到的单独线程上,获得相同的 CurrentPrincipal。但是,在后台,您会通过ExecutionContext.Run(...) 获得上下文,其中指出:

执行上下文返回到它之前的状态,当 方法完成。

我发现自己处于与 Stephen Cleary 不同的陌生领域 :),但我不明白 SynchronizationContext 与此有何关系。

Stephen Toub 在一篇出色的文章 here 中涵盖了大部分内容。

【讨论】:

  • 我同意。自定义 SynchronizationContext 将无济于事,因为 CurrentPrincipal 将在 SynchronizationContext.Post 作为 await 延续的一部分被调用时恢复。 custom awaiter 可能是一个选项,但总是有一个可能的竞争条件,即任务在等待者的 ICriticalNotifyCompletion.OnCompleted 被调用之前完成。
  • 好文章!谢谢sellotape!
猜你喜欢
  • 2013-10-31
  • 2016-11-07
  • 1970-01-01
  • 2021-04-30
  • 2013-05-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-04
相关资源
最近更新 更多