[英]ThreadStatic in async/await - LogicalCallContext doesn't flow outside of async method?
我正在嘗試為異步/等待實現一個可重入的類似Monitor
的原語。
public async Task<DisposableAction> LockAsync()
{
// await current executing task;
// ...
if (_reentrant)
{
var list = CallContext.LogicalGetData(CallContextListName) as HashSet<AsyncLock>;
if (list != null && list.Contains(this)) return new DisposableAction();
list = list ?? new HashSet<AsyncLock>();
list.Add(this);
CallContext.LogicalSetData(CallContextListName, list);
}
// acquire a "lock" and return IDisposable that removes from list
// ...
}
調用代碼如下所示:
using (await _lock.LockAsync())
{
// ...
return await PossibleRecursiveMethod();
}
我正在調試它,看起來CallContextListName對象存在於LockAsync
方法內的CallContext
中,但在執行到達using
塊內的第一條語句時消失了。
我在 .NET 4.5.2 上執行此操作,所以我猜LogicalCallContext
應該可以工作。 那么有什么問題呢? 應該如何實施?
我正在嘗試為異步/等待實現一個類似監視器的可重入原語。
對於您的問題,這幾乎肯定是錯誤的解決方案。 可重入鎖會導致大量非常微妙的問題(在我的博客中有詳細說明)並且通常表明設計不佳。 可重入鎖只有一個用例:
遞歸鎖在具有並行特性的遞歸算法中很有用,其中出於性能原因需要對共享數據結構進行細粒度鎖定。
我不確定異步重入鎖是否會有一個有效的用例。 也就是說,我已經將它們實現為基於我的 AsyncEx 庫的概念驗證。
關於你的問題的細節,代碼其實有兩個問題:
這是我寫的一個實現:
https://www.nuget.org/packages/ReentrantAsyncLock/
這是我見過的唯一一個同時為您提供所有這三個的東西:
您會注意到其中的后兩個是Monitor.Enter
/ lock
給您的。 我認為將ReentrantAsyncLock
稱為異步等效項是有效的。
研究代碼以了解它是如何工作的。 您還會注意到它使用ExecutionContext
(通過異步調用向下流動)來啟用重入。 為了更直接地回答您的問題,它通過將本地范圍對象同步放入ExecutionContext
(通過AsyncLocal
)使本地范圍對象逃逸到調用上下文(“在異步方法之外流動”)。 這里的這一行是同步調用的:
以下是你如何使用它:
var asyncLock = new ReentrantAsyncLock();
var raceCondition = 0;
// You can acquire the lock asynchronously
await using (await asyncLock.LockAsync(CancellationToken.None))
{
await Task.WhenAll(
Task.Run(async () =>
{
// The lock is reentrant
await using (await asyncLock.LockAsync(CancellationToken.None))
{
// The lock provides mutual exclusion
raceCondition++;
}
}),
Task.Run(async () =>
{
await using (await asyncLock.LockAsync(CancellationToken.None))
{
raceCondition++;
}
})
);
}
Assert.Equal(2, raceCondition);
這當然不是第一次嘗試這樣做。 但就像我說的那樣,這是迄今為止我見過的唯一正確的嘗試。 其他一些實現會在嘗試在Task.Run
調用之一中重新輸入鎖時發生死鎖。 其他人實際上不會提供互斥, raceCondition
變量有時會等於 1 而不是 2:
原因在於LogicalCallContext
僅傳遞更深,因此當執行從LockAsync
返回時,存儲的上下文會丟失。
我最終用非異步方法版本包裝了LockAsync
:
public Task<DisposableAction> EnterAsync()
{
container = new Container(); // class
// I'm using AsyncLocal instead of LogicalCallContext but it's pretty much the same thing
_entered.Value = container;
return EnterAsync(container);
}
async Task<DisposableAction> EnterAsync(Container container)
{
// real work
}
這樣, LogicalCallContext
被設置在與調用者方法相同的異步級別上,並且不會隨着返回而丟失。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.