使用 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
本身不会对线程本地数据进行任何同步。不过,如果您想进行自己的同步,它确实有某种"钩子"。
默认情况下,当您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);
}
}
}
包含 SecurityContext
的 ExecutionContext
包含 CurrentPrincipal
的 ,非常漂亮 - 总是流经所有异步分叉。 因此,在您的Task.Run()
委托中,您 - 在您注意到的单独线程上,得到相同的CurrentPrincipal
. 但是,在后台,您可以通过 ExecutionContext.Run(...( 获得上下文流,其中指出:
执行上下文在以下情况下返回到其先前状态方法完成。
我发现自己与斯蒂芬·克利里(Stephen Cleary(:)处于不同的奇怪领域,但我看不出SynchronizationContext
与此有什么关系。
斯蒂芬·图布(Stephen Toub(在这里的一篇出色的文章中涵盖了大部分内容。