下面是我尝试将异步方法中的 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
}
我知道有线程更改,但认为 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
的内容。在另一个线程(使用 Task.Run 或 ThreadPool.QueueWorkItem)上执行委托时,将从当前线程捕获 ExecutionContext,并将委托包装在
ExecutionContext.Run中。因此,如果您在调用 Task.Run 之前设置 CurrentPrincipal,它仍然会在 Delegate 内部设置。 现在您的问题是您更改了 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);
}
}
}
ExecutionContext
,其中包含
SecurityContext
,其中包含CurrentPrincipal
,几乎总是流经所有异步分叉。因此,在您的 Task.Run()
委托中,您 - 在您注意到的单独线程上,得到相同的 CurrentPrincipal
。然而,在幕后,您可以通过 ExecutionContext.Run(...)获得上下文流,其中指出: 执行上下文返回到之前的状态 方法完成。
我发现自己处于与 Stephen Cleary 不同的奇怪领域:),但我不明白
SynchronizationContext
与此有什么关系。
Stephen Toub 在一篇优秀的文章中介绍了大部分内容这里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)