在物聯網應用場景中,須要維護不少個設備的鏈接,好比基於TCP socket通訊的長鏈接,目的是爲了獲取設備採集的信息,反向控制設備的數字開關或者模擬量。咱們把這些TCP長鏈接都放入了基於線程安全的ConcurrentDictionary激活字典表中,IP地址做爲key,設備箱領域模型做爲value。咱們須要把激活設備箱的字典表維護好,須要將超時沒有心跳的設備,咱們能夠稱之爲脫網設備,給清理出激活字典表,寫入到脫網告警字典表中去。當脫網設備下次再有心跳時,能夠再次移入到激活字典表中,從而再產生恢復告警,進行一系列其餘動做。html
由於要模擬海量設備的TCP場景,咱們利用模擬器生成了12000臺模擬設備。8臺真實設備。數據庫
詳細心跳上報流程詳見上述框架圖緩存
忽然發現我能夠寫一個物聯網的採集系統的系列了,組織一個目錄。但願本身堅持下去吧。安全
原理很簡單,遍歷字典表中超過設置的檢測週期,篩選到一個字典的IEnumerable中去,而後在激活字典表中刪除對應超時key(這裏就是指IP地址)便可。固然這裏的_internal週期能夠*N,多個週期,自行在配置文件中設置便可,配置文件以下:網絡
"ipboxNumStaticInternal": 12
public static void DeleteDeadBoxFromActiveBox(in _internal) { { var outTime = DateTime.Now.AddSeconds(-_internal); var iboxTimeOutList = iboxActiveDictionary.Where(q => (outTime > q.Value.UpdateTime));//.Select(x=> iboxActiveDictionary[x.Key]) ; foreach (var item in iboxTimeOutList) { iboxActiveDictionary.Remove(item.Key); } } }
這裏主要開啓了一個系統定時器,主動會去調用清理脫網設備方法,調用時間間隔即ipboxNumStaticInternal。代碼以下:session
public void systemTimerStart() { var interval = ReadTheInternalFromSetting(); _systemTimer = new Timer(state => { IBoxActiveDicManager.DeleteDeadBoxFromActiveBo(_internal); Console.WriteLine("{1},激活設備數量:{0}\n",IBoxActiveDicManager.iboxActiveDictionary.Count,DateTime.Now); }, null, interval, interval); Console.WriteLine("PemsCom採集系統時鐘已經開啓"); LoggerHelper.Info("PemsCom採集系統時鐘已經開啓"); } /// <summary> /// 配置文件讀入時間間隔方法 /// </summary> /// <returns></returns> private int ReadTheInternalFromSetting() { _internal = int.Parse(Appsettings.app(new string[] {"ipboxNumStaticInternal" })); Console.WriteLine("PemsCom採集系統時鐘配置參數已經讀"); LoggerHelper.Info("PemsCom採集系統時鐘配置參數已經讀"); return Convert.ToInt32(TimeSpan.FromSecond(_internal).TotalMilliseconds); }
這裏會有不少的線程讓CPU來輪片執行,好比:多線程
舉個實際的例子,以圖爲證併發
12008臺設備,每秒處理接受網絡包的峯峯值是9218個包,就是在某一秒,CPU共輪片執行了9218個線程。好比是雙核4線程的,則9218/4=2304.5。即CPU在1秒輪片執行了2305次。即0.43毫秒就輪片執行一次。app
其實3.1已經解釋了高併發。在某一秒,須要處理的接收事件有接近1萬件。而這一時刻的執行順序是無序的,9218裏的這麼多線程,咱們不知道哪一個先執行,哪一個後執行。若是不認爲地加一些邏輯控制,好比咱們今天要介紹的互斥鎖,就會出現一些異常現象。框架
這裏只描述現象,緣由會在下面5.分析異常緣由 作具體描述。
異常所在的位置:心跳處理類以下。
public class HeartHandler { static string _deviceIndex = Appsettings.app(new string[] { "DeviceIndex" }); private static IBoxActive iboxActive; public static void Register(TcpHeartPacket heartPacket,int sessId) { UInt32 IP; UInt64 mac; if (_deviceIndex == "IP") { IP =(UInt32)BitConverter.ToUInt32(heartPacket.IP, 0); if (IBoxActiveDicManager.GetBoxActive(IP, out iboxActive) != true) { IBoxActiveDicManager.iboxActiveDictionary.TryAdd(IP, iboxActive); iboxActive.SessID = sessId; } } else { mac = (UInt64)BitConverter.ToUInt64(heartPacket.Mac, 0); if (IBoxActiveDicManager.GetBoxActive(mac, out iboxActive) != true) { IBoxActiveDicManager.iboxActiveDictionary.TryAdd(mac, iboxActive); iboxActive.SessID = sessId; } } //引用類型,智能指針,使用方便 iboxActive.UpdateTime = DateTime.Now; } }
/// <summary> /// 查詢激活設備箱字典中是否有存在上報的設備箱, /// 存在返回true,不存在返回false,而且新建好設備箱模型 /// </summary> /// <param name="mac"></param> /// <param name="iboxActive"></param> /// <returns></returns> public static bool GetBoxActive(UInt32 IP, out IBoxActive iboxActive) { if (iboxActiveDictionary.TryGetValue(IP, outiboxActive)) { return true; } iboxActive = new IBoxActive(); iboxActive.IP = IP; if (iboxActive.IP != IP) { LoggerHelper.Error(string.Format("實例化賦值不成功.iboxActive.IP:{0};IP{1}", iboxActive.IP, IP)); } return false; }
有沒有感受很奇怪,上一句都賦值了,下一句對比就不相等。可是在多線程大併發裏就是有這種可能,下面會詳細分析。
由於12008臺大併發時很容易出錯,因此改爲了1000臺。以下統計數據會有出錯狀況,這一樣也是由於多線程高併發引發的錯誤。
其實第4的三點緣由都是同一個緣由形成,因此在5.1會詳細闡述,5.2,,5.3只作簡單闡述。這裏敲下黑板,分析多線程高併發的異常問題,程序運行的特色就是見縫就插,就像個老司機同樣,歸納起來就是線程與線程之間的無序性。好比咱們設備心跳線程正在更新設備心跳時間的時候。脫網清理線程就把該設備給清理掉了。如此一來,時間無法賦值給空對象(已被脫網線程給清理)。所以只能報空引用異常,對沒錯,就是這麼簡單,耗費了我很長時間去debug跟思考這個異常。
一樣,在建立了設備實例以後,IP賦值完成,恰好脫網清除設備線程運行清除了設備,當對比的時候,引用原來的地址,字典的原來地址已經存了其餘設備箱的IP,因此IP地址不相等。
緣由實際上是5.2形成的,無法成功註冊,固然數量就不對啦。
就是當我在建立激活設備實例(第一次心跳註冊)或者更新心跳時間的時候(非第一次註冊),不要讓無序的脫網清除線程運行。敲黑板:就是保證心跳處理註冊過程的原子性。對,其實這裏很像關係型數據庫的事務,原子性。原子性就是對抗程序無序形成異常的有力武器。咱們能夠在註冊心跳處理方法上加個互斥鎖,讓編譯器跟運行時去安排更加合理的執行順序。
代碼很簡單。
//定義一把鎖 public static Mutex activeIpboxDicMutex = new Mutex(); //設備箱註冊加鎖。異常所有消除 IBoxActiveDicManager.activeIpboxDicMWaitOne(); HeartHandler.Register(tcpHeartPacsessionId); IBoxActiveDicManager.activeIpboxDicMReleaseMutex();
這裏插入一下事務的使用,也是很相似的,把咱們的主業務加中中間,類比方便你們理解記憶。就像夾心餅乾(瞎扯)。
unitOfWork.BeginTransaction(); // Adds new device unitOfWork.DeviceRepository.Add(device); // Commit transaction unitOfWork.Commit();
固然也能夠給設備箱脫網清除線程加鎖。
IBoxActiveDicManager.activeIpboxDicMutex.WaitOne(); IBoxActiveDicManager.DeleteDeadBoxFromActiveBox(_internal); IBoxActiveDicManager.activeIpboxDicMutex.ReleaseMutex();
考慮到脫網清除線程會損耗部分性能,我也測試了去掉該鎖的狀況,也不會有第4的3個異常,至此問題所有解決。
模擬設備數量小測不出這個問題,如此看出海量設備的重要性,由於現實狀況確定會出現以上三個問題,並且都是很嚴重很致命的問題。好的測試方法能夠把問題扼殺在搖籃中;
多線程高併發時容易出現這樣那樣的異常,要懷着敬畏之心去思考,去解決問題;
版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接和本聲明。