使用async / await设置Thread.CurrentPrincipal
下面是我试图在异步方法中将Thread.CurrentPrincipal设置为自定义UserPrincipal对象的简化版本,但是自定义对象在离开await后会丢失,即使它仍然在新的threadID 10上。
有没有办法在await中更改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 }
您可以使用自定义awaiter来传递CurrentPrincipal
(或任何线程属性)。 下面的例子展示了如何做到这一点,受到Stephen Toub的CultureAwaiter
启发。 它TaskAwaiter
内部使用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 RunAndFlowPrincipal( Func func, CancellationToken token = default(CancellationToken)) { return RunAndFlow( func, () => Thread.CurrentPrincipal, s => Thread.CurrentPrincipal = s, token); } // flowing anything public static FlowingAwaitable RunAndFlow( Func func, Func saveState, Action restoreState, CancellationToken token = default(CancellationToken)) { // wrap func with func2 to capture and propagate exceptions Func, TState>> func2 = () => { Func 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, TState>(getResult, saveState()); }; return new FlowingAwaitable( Task.Run(func2, token), restoreState); } public class FlowingAwaitable : ICriticalNotifyCompletion { readonly TaskAwaiter, TState>> _awaiter; readonly Action _restoreState; public FlowingAwaitable( Task, TState>> task, Action restoreState) { _awaiter = task.GetAwaiter(); _restoreState = restoreState; } public FlowingAwaitable 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); } } }
我知道有线程更改,但认为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也没有。 通常认为以某种方式传递值更合适 – 无论这是对方法是显式的,还是注入到包含此方法的类型的成员变量中。
如果你真的不想传递这个值,那么你可以采取另一种方法:一个async
等价的ThreadLocal
。 核心思想是将不可变值存储在LogicalCallContext
, LogicalCallContext
由异步方法适当地inheritance。 我在我的博客上报道了这个“AsyncLocal” (有传言说AsyncLocal
可能会在.NET 4.6中出现,但在此之前你必须自己动手)。 请注意,您无法使用AsyncLocal
技术读取Thread.CurrentPrincipal
; 您必须更改所有代码才能使用MyAsyncValues.CurrentPrincipal
。
Thread.CurrentPrincipal存储在ExecutionContext中,该执行文本存储在线程本地存储中。
在另一个线程上执行委托时(使用Task.Run或ThreadPool.QueueWorkItem),从当前线程捕获ExecutionContext,并将委托包装在ExecutionContext.Run中 。 因此,如果在调用Task.Run之前设置CurrentPrincipal,它仍将在Delegate中设置。
现在您的问题是您更改了Task.Run中的CurrentPrincipal并且ExecutionContext仅以一种方式流动。 我认为这是大多数情况下的预期行为,解决方案是在开始时设置CurrentPrincipal。
在更改Task中的ExecutionContext时,您最初想要的是不可能的,因为Task.ContinueWith也捕获了ExecutionContext。 要做到这一点,你必须在委托运行后立即以某种方式捕获ExecutionContext,然后在自定义awaiter的延续中将其回流,但那将是非常邪恶的。
ExecutionContext
包含SecurityContext
,它包含CurrentPrincipal
,几乎总是流经所有异步分支。 所以在你的Task.Run()
委托中,你 – 在你注意到的一个单独的线程上,得到相同的CurrentPrincipal
。 但是,在引擎盖下,您可以通过ExecutionContext.Run(…)获取上下文,其中指出:
方法完成时,执行上下文将返回其先前的状态。
我发现自己处于与Stephen Cleary :)不同的奇怪领域,但我不知道SynchronizationContext
与此有什么关系。
Stephen Toub在这里的一篇优秀文章中涵盖了大部分内容。