1. 程式人生 > >你的眼睛背叛你的心:解決 .NET Core 中 GetHostAddressesAsync 引起的 EnyimMemcached 死鎖問題

你的眼睛背叛你的心:解決 .NET Core 中 GetHostAddressesAsync 引起的 EnyimMemcached 死鎖問題

在我們將站點從 ASP.NET + Windows 遷移至 ASP.NET Core + Linux 的過程中,目前遇到的最大障礙就是 —— 沒有可用的支援 .NET Core 的 memcached 客戶端。

我們一直用的是 EnyimMemcached ,在沒有其它選擇的情況下,我們自己嘗試著將 EnyimMemcached 遷移至 .NET Core。。。基於 .NET Core 修改好了程式碼,在開發環境下測試通過,在 Linux 伺服器上自己訪問很正常(沒有併發訪問量),但是隻要接入一定的訪問量就會發生死鎖(deadlock),瀏覽器請求卡死。

這個問題困擾了我們很長時間,昨天才定位到是發生在將 memcached 伺服器名稱解析為 IP 地址的時候。

var addresses = System.Net.Dns.GetHostAddressesAsync(host).Result;

這是我們在將 EnyimMemcached 遷移至 .NET Core 時修改過的程式碼,之前呼叫的是同步方法:

var addresses = System.Net.Dns.GetHostEntry(host);

由於在 .NET Core Framework 的 System.Net.Dns 中沒有同步方法,只有非同步方法,所以我們只能這樣呼叫非同步方法。

看到上面的程式碼,你也許會詫異:怎麼用 .Result ,為什麼不用 await ?不死鎖才怪呢。。。

你的詫異非常正確。我們也深知 .Result 的危害,在平時的程式碼中堅決不用。但當時在修改 EnyimMemcached 的程式碼時,由於這個方法是在 MemcachedClient 的建構函式中呼叫的,沒法改為 await 呼叫,被迫用了 .Result ,然後又把這個地方的修改給忘了。。。昨天才剛剛發現,立馬意識到罪魁禍首非常有可能就是這裡的 .Result ,於是以此為突破口,想盡一切辦法實現在同步方法中呼叫非同步辦法,並且在博問中尋求支援 —— 在同步方法中呼叫非同步方法時如何避免死鎖問題 。

結果,用盡一切能想到與能找到的同步方法呼叫非同步方法的方法,都沒能解決死鎖問題。如果實在找不到解決方法,我們準備採用最後一招也是最醜陋的一招 —— 不用 Dns.GetHostAddressesAsync() ,用 ProcessStartInfo 呼叫命令列命令解析 IP ,比如在 Linux 上用 getent hosts 主機名

 。

在準備放棄之前,今天又想了想還有哪些可能帶來線索的地方漏掉了呢?突然想到有個重要地方竟然忘了,還沒看 Dns.GetHostAddressesAsync() 的原始碼實現。雖然不報太大希望,不就是個非同步方法嗎,但還是要看一下。

於是從 github 上籤出 corefx 的原始碼,開啟 Dns.GetHostAddressesAsync() 原始碼一看,感覺有點怪怪的,怎麼用了 Task.Factory.FromAsync() ?

public static Task<IPAddress[]> GetHostAddressesAsync(string hostNameOrAddress)
{
    NameResolutionPal.EnsureSocketsAreInitialized();
    return Task<IPAddress[]>.Factory.FromAsync(
        (arg, requestCallback, stateObject) => BeginGetHostAddresses(arg, requestCallback, stateObject),
        asyncResult => EndGetHostAddresses(asyncResult),
        hostNameOrAddress,
        null);
}

開始沒反應過來,只是把這段程式碼貼到博問的補充問題中,在貼完後突然反應過來了,咦,怎麼沒有 async 關鍵字?方法名最後是 Async,我們一直以為是 async 方法,而且絲毫沒有懷疑過。。。

沒有 async ,只是返回引數是 Task 型別,那在同步方法中呼叫完全沒問題,只要在訪問 .Result 之前呼叫一下 .Wait() 方法就行了,於是改為下面的程式碼:

Task<IPAddress[]> task = System.Net.Dns.GetHostAddressesAsync(host);
task.Wait();
var addresses = task.Result;
Task<IPAddress[]> task = System.Net.Dns.GetHostAddressesAsync(host);
if (task.Wait(5000))
{
    var addresses = task.Result;
}

死鎖問題立馬解決!

方法名以 Async 結尾,卻不是 async 方法,當時的感想就是 —— 你的眼睛背叛你的心。如果不是我自己的誤解(只要以 Async 結尾,就應該是 async 方法),那就是一種流氓行為,就如 HttpClient 的流氓 —— 實現了 IDispose 介面,卻沒真正 Dispose 。 

不管怎麼樣,這個影響我們遷移至 .NET Core 的最大障礙終於消除了,值得慶祝!

支援 .NET Core 的 EnyimMemcached 的程式碼還需要一些修改與完善,等修改好了,我們會把原始碼與 NuGet 包都發布出來。