使用 async/await 设置 Thread.CurrentPrincipal

本文关键字:Thread CurrentPrincipal 设置 await async 使用 | 更新日期: 2023-09-27 18:02:02

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

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

[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
}

使用 async/await 设置 Thread.CurrentPrincipal

我知道有线程更改,但认为异步/等待会为我处理同步。

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 也没有。通常认为以某种方式传递值更合适 - 无论是显式传递给方法,还是注入到包含此方法的类型的成员变量中。

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

Thread.CurrentPrincipal 存储在 ExecutionContext 中,该上下文存储在 Thread Local Storage 中。

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

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

在更改任务中的 ExecutionContext 时,您最初想要的是不可能的,因为 Task.ContinueWith 也会捕获 ExecutionContext。为此,您必须在委托运行后立即以某种方式捕获 ExecutionContext,然后将其流回自定义等待程序的延续中,但这将是非常邪恶的。

您可以使用自定义等待程序来流CurrentPrincipal(或任何线程属性,就此而言(。下面的示例显示了如何完成它,灵感来自 Stephen Toub 的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);
        }
    }
}

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

执行上下文在以下情况下返回到其先前状态方法完成。

我发现自己与斯蒂芬·克利里(Stephen Cleary(:)处于不同的奇怪领域,但我看不出SynchronizationContext与此有什么关系。

斯蒂芬·图布(Stephen Toub(在这里的一篇出色的文章中涵盖了大部分内容。