最近在測試一個功能代碼時發現一個非常奇怪的問題,主要是Task.Run引起一些不符合邏輯的錯誤,以下針對這一問題排查的總結。
問題代碼
可以建個控制檯程序來運行以下代碼
class Program
{
static User user = new User();
static void Main(string[] args)
{
for (int i = 0; i < 50; i++)
{
Task.Run(user.Init);
}
System.Threading.Thread.Sleep(-1);
}
}
public class User
{
private bool mInit = false;
private Task OnInit()
{
Console.WriteLine("User init");
System.Threading.Thread.Sleep(1000);
return Task.CompletedTask;
}
public void Init()
{
lock (typeof(User))
{
if (!mInit)
{
var task = Task.Run(this.OnInit);
if (!task.Wait(5000))
{
throw new TimeoutException("user init error!");
}
mInit = true;
}
}
}
}
以上代碼執行的結果非常奇怪,當在Debug模式下運行,會拋出超時錯誤。
運行在release模式下則會引起OnIint方法被執行多次,lock完全起不了作用。。
和朋友討論過程中說lock不要和Task.Run混用,但Task.Wait的實現是基於線程信號量的和async/await是有着本質的差異。抱着解決問題的思路把Task.Run直接改成了線程池方式運行,但結果還是一樣。由於找不到問題原因最終去dotnet上提個issues,看一下能提供什麼意見。
問題的發現
對於一個程序員來說問題沒解決怎能安心呢,隔一天issues沒有響應於是開啓的解決問題的碰撞模式。在throw timeout裏打個斷點看一下情況,結果無意中發現Task的狀態是WaitingForActivation
狀態描述是等待內部調度激活,意思是說這代碼並不是不執行或執行有問題,而因爲某些狀態導致Task還在等待執行中。然後針對這一問題在網查找了一下才發現這問題的原因,主要問題是for 50已經把線和池中的線程抽光了,然然後在Init方法使用Task.Run的時候就只能等待。。。加上方法後面Task.Wait導致當前線程無法迴歸到池,所以就只能引起超時間異常!如果這裏的Task.Wait不加上個超時,那這測試代碼就直接處於假死狀態無法繼續工作,一個等待一個試圖獲取線程操作從而形成一個類似於死鎖的問題!
總結
當你在使用Task.Run時出現一些非常意想不到的結果時可以通過Task.Status狀態可以更好的定位到問題。Task默認也是基於線程池的,所以在使用Task.Run和Task.Wait的就要注意這一點,雖然可以通過加大線程池的最小數量來解決低併發問題,但高併發下還是會存在線程資源不足的情況;爲了確保不出現類似於死鎖的問題,請在使用Task.Wait必須加上超時時間,並且是越短越好,畢竟Wait方法是基於線程阻塞。
BeetleX
開源跨平臺通訊框架(支持TLS)
輕鬆實現高性能:tcp、http、websocket、redis、rpc和網關等服務應用
https://beetlex.io
如果你想了解某方面的知識或文章可以把想法發送到