.Net常見線程安全問題整理

最近線上又出現了幾次線程安全問題 導致的服務異常,
線程安全問題都是隱藏的炸彈,有可能幾個月都不出問題,也有可能連續幾天爆炸好幾次,
問題出現的結果完全是無法確定的,包括但不限於如下結果:

  1. 應用異常,且無法自恢復,必須重啓站點或服務;
  2. 陷入死循環,導致CPU佔用100%,從而整臺服務器崩潰;
  3. 錯誤數據入庫,導致一系列的排查、數據修復的困難,甚至可能無法修復數據;

因此,很有必要做幾次全局的篩查,做一些特徵值搜索和處理,
簡單梳理了一下,凡是符合如下5種特徵的代碼,都有線程不安全的可能性:

1、類的靜態變量:

public class Iamclass
{
    static Dictionary<string, string> _cache = new Dictionary<string, string>();
    public static void Operation()
    {
        _cache.Add(new Guid().ToString(), "1");// 線程不安全代碼
    }
}

2、類的靜態屬性:

public class Iamclass
{
    static Dictionary<string, string> Cache {get; set;} = new Dictionary<string, string>();
    public static void Operation()
    {
        Cache.Add(new Guid().ToString(), "1");// 線程不安全代碼
    }
}

3、單例對象的靜態變量:

public class XxxService
{
    IIamclass instance = IocHelper.GetSingleInstance<IIamclass>(); // 獲取單例
}
public class Iamclass : IIamclass
{
    Dictionary<string, string> _cache = new Dictionary<string, string>();
    public void Operation()
    {
        _cache.Add(new Guid().ToString(), "1");// 線程不安全代碼
    }
}

4、單例對象的靜態屬性:

public class XxxService
{
    IIamclass instance = IocHelper.GetSingleInstance<IIamclass>(); // 獲取單例
}
public class Iamclass : IIamclass
{
    Dictionary<string, string> Cache {get; set;} = new Dictionary<string, string>();
    public void Operation()
    {
        Cache.Add(new Guid().ToString(), "1");// 線程不安全代碼
    }
}

5、多線程共享的局部變量

public class XxxService
{
    public void Operation()
    {
        var cache = new Dictionary<string, string>();
        System.Threading.Tasks.Parallel.For(1, 10, idx =>
        {
            cache.Add(new Guid().ToString(), "1"); //線程不安全代碼
        });
    }
}

列舉下處理過的幾次線程安全問題,都是如下2類問題:
1、應用錯誤且無法恢復的,通常異常爲:索引超出了數組界限:

public class MessageService : BaseService
{
    private static Dictionary<string, Timer> _timerDict = new Dictionary<string, Timer>();
    public async void SendMessageAsync(string msgId, MessageInputDto2 input)
    {
	    var timer = new Timer(60 * 1000) { AutoReset = true };
        _timerDict[msgId] = timer;	   // 問題代碼 
	    timer.Elapsed += (sender, eventArgs) =>
	    {
	    	try
	    	{
	    		/* 具體業務代碼 */
	    		timer.Stop();
                timer.Close();
                _timerDict.Remove(msgId);
	    	}
	    	catch(Exception exp)
	    	{
	    		// 異常處理代碼
	    	}
	    }
	}
}

解決方法,一般是加鎖
注:如果加lock 可能出現瓶頸,要進行流程梳理,是否要更換實現方案:

lock(_timerDict)
{
	_timerDict[msgId] = timer;	   // 問題代碼 
}
timer.Elapsed += (sender, eventArgs) =>
{
	try
	{
		/* 具體業務代碼 */
		timer.Stop();
        timer.Close();
        lock(_timerDict)
        {
        	_timerDict.Remove(msgId);
        }
	}
	catch(Exception exp)
	{
		// 異常處理代碼
	}
}

2、陷入死循環,導致服務器CPU 100%卡頓問題:
有個常見業務,獲取一串沒有使用過的隨機數或隨機字符串,比如用戶身份Token,比如抽獎等等
下面是常見的獲取不重複的隨機數代碼,
在_rnd.Next 沒有加鎖,其內部方法InternalSample會導致返回結果都是0,從而導致while陷入死循環:

public class CodeService
{
    private static Random _rnd = new Random(Guid.NewGuid().GetHashCode());
    public static GetCode()
    {
    	var ret = "";
	    var redis = IocHelper.GetSingleInstance<IRedis>();
	    // 獲取一個未使用過的序號
    	do
    	{
    	    ret = _rnd.Next(10000).ToString();  // 問題代碼
    	}while(!redis.Add(ret, "", TimeSpan.FromSeconds(3600)));
    	return ret;
    }
}

解決方法,雙重校驗:加鎖,並判斷循環次數:

public class CodeService
{
    private static Random _rnd = new Random(Guid.NewGuid().GetHashCode());
    public static GetCode()
    {
    	var ret = "";
	    var redis = IocHelper.GetSingleInstance<IRedis>();
	    var maxLoop = 10;
	    // 獲取一個未使用過的序號
    	do
    	{
    		lock(_rnd)
    		{
    	        ret = _rnd.Next(10000).ToString();
    	    }
    	}while(!redis.Add(ret, "", TimeSpan.FromSeconds(3600)) && (maxLoop--) > 0);
    	if(maxLoop <= 0)
    	{
    		throw new Exception("10次循環,未找到可用數據:" + ret);
    	}
    	return ret;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章