又踩.NET Core的坑:在同步方法中調用異步方法Wait時發生死鎖(deadlock)


之前在將 Memcached 客戶端 EnyimMemcached 遷移 .NET Core 時被這個“坑”坑的刻骨銘心(詳見以下鏈接),當時以為只是在構造函數中調用異步方法(注:這里的異步方法都是指基於Task的)才會出線死鎖(deadlock)問題。

最近在使用 redis 客戶端 StackExchange.Redis 時也遇到了這個問題, 詳見 ASP.NET Core中StackExchange.Redis連接redis服務器的問題 。

StackExchange.Redis 中死鎖問題發生在下面的代碼:

private static ConnectionMultiplexer ConnectImpl(Func<ConnectionMultiplexer> multiplexerFactory, TextWriter log)
{
    IDisposable killMe = null;
    try
    {
        var muxer = multiplexerFactory();
        killMe = muxer;
        // note that task has timeouts internally, so it might take *just over* the regular timeout
        var task = muxer.ReconfigureAsync(true, false, log, null, "connect"); if (!task.Wait(muxer.SyncConnectTimeout(true)))
        {
            task.ObserveErrors();
            if (muxer.RawConfig.AbortOnConnectFail)
            {
                throw ExceptionFactory.UnableToConnect("Timeout");
            }
        }
        if (!task.Result) throw ExceptionFactory.UnableToConnect(muxer.failureMessage);
        killMe = null;
        return muxer;
    }
    finally
    {
        if (killMe != null) try { killMe.Dispose(); } catch { }
    }
}

ConnectImpl() 是一個同步方法,muxer.ReconfigureAsync() 是一個 async 異步方法。在 Linux 上運行時, task.Wait(muxer.SyncConnectTimeout(true)) 會因為等待超時而返回 false ,從而出現下面的錯誤:

StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s); to create a disconnected multiplexer, disable AbortOnConnectFail. Timeout
   at StackExchange.Redis.ConnectionMultiplexer.ConnectImpl(Func`1 multiplexerFactory, TextWriter log)

如果改為 task.Wait() ,在 ASP.NET Core 程序中調用時,請求會因為死鎖而卡死。

這是一個典型的同步方法調用異步方法在 Wait 時的死鎖問題(詳見 Don't Block on Async Code),通常的解決方法是使用 .ConfigureAwait(false); (異步任務執行完成時不獲取SynchronizationContext)。StackExchange.Redis 的開發者當然知道這一點,在 muxer.ReconfigureAsync()  中調用每一個異步方法時都加上了 .ConfigureAwait(false); 。但我們遇到的實際情況顯示,這一招在 .NET Framework 中管用,在 .NET Core 中卻不管用,這可能與 .NET Core 在異步機制上的改變有關,比如在異步方法中 System.Threading.SynchronizationContext.Current 的值總是為 null ,詳見 ASP.NET Core 1.0 SynchronizationContext 。

這個問題在 Liunx 上很容易出現,而在 Windows 上需要一定的並發請求才會出現。

后來在 Microsoft.AspNetCore.DataProtection.AzureStorage 中也發現了在同步方法中調用異步方法的代碼:

public IReadOnlyCollection<XElement> GetAllElements()
{
    var blobRef = CreateFreshBlobRef();

    // Shunt the work onto a ThreadPool thread so that it's independent of any
    // existing sync context or other potentially deadlock-causing items.

    var elements = Task.Run(() => GetAllElementsAsync(blobRef)).GetAwaiter().GetResult();
    return new ReadOnlyCollection<XElement>(elements);
}

參考上面的代碼,在 StackExchange.Redis 中的 ConnectImpl() 方法中改為 Task.Run() 調用異步方法,死鎖問題依舊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM