使用 async/await 设置 Thread.CurrentPrincipal

问题描述 投票:0回答:5

下面是我尝试将异步方法中的 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
}
c# asynchronous async-await claims-based-identity current-principal
5个回答
14
投票

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

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

默认情况下,当您

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

所以,如果你想定义一个“上下文”,你可以通过定义你自己的

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

但是,请注意,有一个明确的运动从 SynchronizationContext

远离
。 ASP.NET vNext 没有。欧文从来没有这样做过(据我所知)。自托管 SignalR 也没有。通常认为以“some”方式传递值更合适 - 无论这对于方法是显式的,还是注入到包含此方法的类型的成员变量中。 如果您

真的

不想传递该值,那么您还可以采取另一种方法:async-相当于

ThreadLocal
。核心思想是将不可变值存储在
LogicalCallContext
中,该值由异步方法适当继承。我在我的博客上介绍了这个
“AsyncLocal”
(有传言称 AsyncLocal 可能会出现在 .NET 4.6 中,但在那之前你必须自己推出)。请注意,您无法使用
Thread.CurrentPrincipal
技术来读取
AsyncLocal
;您必须更改所有代码才能使用类似
MyAsyncValues.CurrentPrincipal
的内容。
    


10
投票

在另一个线程(使用 Task.Run 或 ThreadPool.QueueWorkItem)上执行委托时,将从当前线程捕获 ExecutionContext,并将委托包装在

ExecutionContext.Run

中。因此,如果您在调用 Task.Run 之前设置 CurrentPrincipal,它仍然会在 Delegate 内部设置。 现在您的问题是您更改了 Task.Run 中的 CurrentPrincipal 并且 ExecutionContext 仅以一种方式流动。我认为这是大多数情况下的预期行为,解决方案是在开始时设置 CurrentPrincipal。

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


8
投票
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); } } }



6
投票
ExecutionContext

,其中包含

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

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

我发现自己处于与 Stephen Cleary 不同的奇怪领域:),但我不明白
SynchronizationContext

与此有什么关系。

Stephen Toub 在一篇优秀的文章中介绍了大部分内容

这里


0
投票

public static void SetContext(int id, string name) { var context = new LoadContext(id); var culture = context.Region.CultureInfo; Thread.CurrentPrincipal = new PTPrincipal(name, context); Thread.CurrentThread.CurrentCulture = culture; Thread.CurrentThread.CurrentUICulture = culture; }

这有一个与提问者类似的问题,即当使用异步等待时,此上下文设置会丢失。解决方法是 
等待的调用在调用堆栈中的每个等待的调用上使用ConfigureAwait(false)

private async Task DoSomething() { await Task.Delay(1000).ConfigureAwait(false); } ..... await DoSomething().ConfigureAwait(false);

所以,对我们来说,解决办法是始终使用 
.ConfigureAwait(false)

    

© www.soinside.com 2019 - 2024. All rights reserved.