[英]Setting Thread.CurrentPrincipal with async/await
下面是我試圖在異步方法中將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
}
我知道有線程更改,但認為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
,該方法由異步方法適當地繼承。 我在我的博客上報道了這個“AsyncLocal” (有傳言說AsyncLocal
可能會在.NET 4.6中出現,但在那之前你必須自己動手)。 請注意,您無法使用AsyncLocal
技術讀取Thread.CurrentPrincipal
; 您必須更改所有代碼才能使用MyAsyncValues.CurrentPrincipal
。
Thread.CurrentPrincipal存儲在ExecutionContext中,ExecutionContext存儲在Thread Local Storage中。
在另一個線程上執行委托時(使用Task.Run或ThreadPool.QueueWorkItem),從當前線程捕獲ExecutionContext,並將委托包裝在ExecutionContext.Run中 。 因此,如果在調用Task.Run之前設置CurrentPrincipal,它仍將在Delegate中設置。
現在您的問題是您更改了Task.Run中的CurrentPrincipal並且ExecutionContext僅以一種方式流動。 我認為這是大多數情況下的預期行為,解決方案是在開始時設置CurrentPrincipal。
在Task中更改ExecutionContext時,您最初想要的是不可能的,因為Task.ContinueWith也捕獲了ExecutionContext。 要做到這一點,你必須在委托運行之后立即以某種方式捕獲ExecutionContext,然后在自定義awaiter的延續中將其回流,但那將是非常邪惡的。
您可以使用自定義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<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在這里的一篇優秀文章中涵蓋了大部分內容。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.