在物联网应用场景中,须要维护不少个设备的链接,好比基于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 版权协议,转载请附上原文出处连接和本声明。