簡體   English   中英

異步/等待中的 ThreadStatic - LogicalCallContext 不會在異步方法之外流動?

[英]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 庫的概念驗證。

關於你的問題的細節,代碼其實有兩個問題:

  1. 您應該只將不可變數據存儲在邏輯調用上下文中(正如我在博客中所描述的那樣)。
  2. “異步本地”數據僅流向異步調用。 它不能“向上”流動。 我的概念驗證通過僅在異步鎖獲取完成時更新邏輯調用上下文(使用自定義等待)來回避這一點。

這是我寫的一個實現:

https://www.nuget.org/packages/ReentrantAsyncLock/

這是我見過的唯一一個同時為您提供所有這三個的東西:

  • 異步性
  • 重入
  • 互斥

您會注意到其中的后兩個是Monitor.Enter / lock給您的。 我認為將ReentrantAsyncLock稱為異步等效項是有效的。

研究代碼以了解它是如何工作的。 您還會注意到它使用ExecutionContext (通過異步調用向下流動)來啟用重入。 為了更直接地回答您的問題,它通過將本地范圍對象同步放入ExecutionContext (通過AsyncLocal )使本地范圍對象逃逸到調用上下文(“在異步方法之外流動”)。 這里的這一行是同步調用的:

https://github.com/matthew-a-thomas/cs-reentrant-async-lock/blob/d103d21b1b8f5fb755ee61618a27479a5d793e98/ReentrantAsyncLock/ReentrantAsyncLock.cs#L100

以下是你如何使用它:

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM