2、线程池线程阻塞

上一篇提到我为了防止定时器事件重入而加了锁。某一天我再打开监控页面发现监控频率为27秒的监控项的最后监控时间都停止在了前一天晚上的零点左右。

在本机也没有重现这种情况,我觉得可能是偶然现象,但持续观察了几天之后发现持续运行24小时左右这种情况是毕现,更严重的情况是所有的定时器貌似都是跑着跑着跑“累了”就不跑了,而且通过任务管理器查看发现进程创建的线程达到了200多个,至于原因我的第一反应是死锁导致线程池的线程被阻塞(block)了,因为前面我为了防止出现定时器事件重入而加了锁。而当线程池的线程被阻塞,线程池会创建额外的线程。

锁和线程同步

以前交通不发达村与村之间只有一条马路相连,村民之间来往是很不方便也不需要什么管理,后来发展了修了很多马路为了维护秩序十字路口有了红绿灯,某一天“领导”下来视察工作还会封路。多线程环境下为了维护秩序和同步线程本质上就有上面的“红绿灯”和“封路”的思想,也就是信号灯和加锁机制。信号量机制更关注共享资源,锁则更关注同步和一致性,个人理解它们本质还是一样的东西只是概念上不一样而已。C#中封装信号量和锁机制的类有Moniotr、Mutex、Semaphore、ManualResetEvent和AutoResetEvent以及上面我使用到的一个关键字lock,其中Mutex、Semaphore、ManualResetEvent和AutoResetEvent这些类都是继承自WaitHandle。而lock关键字本质就是Monitor的用法,只是个类似using语法糖用以保证锁得以释放不至于导致死锁。这从IL代码里可以很明显的看出来:

 private static object lockObj = new object();static void Main(string[] args){lock (lockObj){}}

.

Monitor除了提供Enter方法外还提供了一个TryEnter方法,下面是MSDN上的一个例子:

// Try to add an element to the queue: Add the element to the queue
// only if the lock becomes available during the specified time
// interval.
public bool TryEnqueue(T qValue, int waitTime)
{// Request the lock.if (Monitor.TryEnter(m_inputQueue, waitTime)){try{m_inputQueue.Enqueue(qValue);}finally{// Ensure that the lock is released.Monitor.Exit(m_inputQueue);}return true;}else{return false;}
}

在指定的时间内获取排他锁,如果获取到的话返回true否则返回false,主要就是为了防止出现死锁。于是我为了防止出现死锁我一开始用了下面这样的一个封装了Montor.TryEnter方法的类,它实现了IDisposable接口保证资源能够释放:

/// <summary>  /// 会自动释放的锁,可设置等待超时  /// </summary>  public class Lock : IDisposable{/// <summary>  /// 默认超时设置  /// </summary>  public static int DefaultMillisecondsTimeout = 15000; // 15S  private object _obj;/// <summary>  /// 构造   /// </summary>  /// <param name="obj">想要锁住的对象</param>  public Lock(object obj){TryGet(obj, DefaultMillisecondsTimeout, true);}/// <summary>  /// 构造  /// </summary>  /// <param name="obj">想要锁住的对象</param>  /// <param name="millisecondsTimeout">超时设置</param>  public Lock(object obj, int millisecondsTimeout){TryGet(obj, millisecondsTimeout, true);}/// <summary>  /// 构造  /// </summary>  /// <param name="obj">想要锁住的对象</param>  /// <param name="millisecondsTimeout">超时设置</param>  /// <param name="throwTimeoutException">是否抛出超时异常</param>  public Lock(object obj, int millisecondsTimeout, bool throwTimeoutException){TryGet(obj, millisecondsTimeout, throwTimeoutException);}private void TryGet(object obj, int millisecondsTimeout, bool throwTimeoutException){if (System.Threading.Monitor.TryEnter(obj, millisecondsTimeout)){_obj = obj;}else{if (throwTimeoutException){throw new TimeoutException();}}}/// <summary>  /// 销毁,并释放锁  /// </summary>  public void Dispose(){if (_obj != null){System.Threading.Monitor.Exit(_obj);}}/// <summary>  /// 在获取锁时是否发生等待超时  /// </summary>  public bool IsTimeout{get{return _obj == null;}}}

但是简单的运行程序一段时间后发现它有没有解决之前的“死锁”现象并不确定,但是之前的事件重入的问题又重现了,我想原因很容易明白。于是我放弃了使用这种看似很“高明”方法。那么在定时器的事件重入和死锁之间就没有什么很好的方法了吗?我在网上找到关于定时器死锁和重入说的很好的一段话:

“……Thread Timers是基于回调函数我更喜欢Thread Timer,比较轻量级方便易用。但是这样的Timer也有问题,就是由于是多线程定时器,就会出现如果一个Timer处理没有完成,到了时间下一个照样会发生,这就会导致重入问题对付重入问题通常的办法是加锁,但是对于 Timer却不能简单的这样做,你需要评估一下首先Timer处理里本来就不应该做太需要时间的事情,或者花费时间无法估计的事情,比同远方的服务器建立一个网络连接,这样的做法尽量避免如果实在无法避免,那么要评估Timer处理超时是否经常发生,如果是很少出现,那么可以用lock(Object)的方法来防止重入如果这种情况经常出现呢?那就要用另外的方法来防止重入了我们可以设置一个标志……”这段话给我们在使用定时器的时候提了几点建议:

  • 不要在定时器事件中做太耗时的操作尤其是网络请求
  • 不要随便的加锁
  • 不要依赖定时器做特别精确的事情

在我的实际应用中在定时器事件中的确要做大量的网络请求,其实对时间的精确度要求并不高,但是不允许频繁的出现重入,因为我的监控组件是长时间运行的,如果不停的从线程池开辟新线程的话,那么线程会越来越多,占用的系统资源也会越来越多。后来发现解决我这个问题的方法很简单,在进入定时器事件的时候将定时器的Enabled属性设为false(停止它引发Elasped事件),离开后再重新置为true。

try{//防止事件重入_setTimer.Enabled = false;foreach (MonitorItem item in _anaList){}}finally{if (_setTimer != null){_setTimer.Enabled = true; }}

在运行了一段时间后一切看似很正常,通过任务管理器没有发现有大量的线程被开辟出来,的确是解决了定时器重入的问题了。但正在我庆幸貌似也解决了之前出现的死锁现象的时候,整个收集组件又停止运转了,这次不同的是几乎所有定时器又都僵死在一个时刻了,给人的感觉是所有的定时器线程都在等”某个东西“。整个收集组件除了一大堆的定时器线程外(ThreadPool线程)还有一个主线程以及一个用来刷新各个定时器数据的线程(Refresh thread),如下图所示:

主线程创建一个Refresh thread(非线程池线程)和多个定时器(ThreadPool thread)。现在的情况是主线程和刷新线程依然运行正常,但是所有的定时器线程都“僵死”掉了。那么会不会是主线程或者刷新线程占用了某个资源一直没有释放,而线程池线程都在等待这个资源而形成了“死锁”。那么到底什么是死锁?造成死锁的原因有哪些呢?网上关于这方面的讨论有很多(看这)。关于早成死锁的原因网上一般都说有四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上面说的进程其实线程是一样的道理。个人理解造成死锁的无非是”天灾“和”人祸“,”天灾“是指有些系统资源时有限的必须加以控制的,而“人祸”是指我们在使用多线程尤其是进行同步的时候操作不当导致的。前面提到的Mutex、Semaphore、ManualResetEvent和AutoResetEvent以及Monitor类都是用来进行线程间的同步,那么他们之间的区别是什么呢?MSDN说Monitor和WaitHandle对象区别:”Monitor 对象是纯托管的,并且完全可移植,从操作系统资源要求来看可能更为高效。WaitHandle 对象表示操作系统可等待的对象,对于在托管和非托管代码之间实现同步非常有用,并提供一些高级的操作系统功能,例如同时等待多个对象的功能。“另外Mutex相对Monitor能够实现进程间的同步。而几个WaitHandle对象之间的区别只是对系统信号量机制不同的封装而已(可以看这)。于是我怀疑我的程序出现的那个情况会不会是”人祸“所致,某个地方不当的使用了这些类,于是我在整个项目中Ctrl+F挨个将这些对象找了一遍,最后定为在了记录日志的代码上:

public static Mutex mutex = new Mutex();/// <summary>/// 将日志信息记录到文件中/// </summary>/// <param name="fileName"></param>/// <param name="logContent"></param>public static void WriteLogToFile(string logContent){FileStream fs = null;StreamWriter streamWriter = null;try{string directory = logPath;if (!Directory.Exists(directory)){Directory.CreateDirectory(directory);}DirectoryInfo directoryInfo = new DirectoryInfo(directory);FileInfo[] fileInfos = directoryInfo.GetFiles();string filePath = string.Format("{0}\\{1}.log", logPath, DateTime.Now.ToString("yyyyMMdd"));mutex.WaitOne();fs = new FileStream(filePath, FileMode.Append);streamWriter = new StreamWriter(fs);streamWriter.BaseStream.Seek(0, SeekOrigin.End);streamWriter.WriteLine(string.Format("{0}:{1}", DateTime.Now.ToString(CultureInfo.CurrentCulture), logContent));mutex.ReleaseMutex();}catch (Exception){//SmtpHelper.SendSysAlarmEmail("记录日志出现异常:" + ex.ToString());return;}}

如果一个线程在mutex.WaitOne()后抛出异常导致没有执行mutex.ReleaseMutex()这行代码那么会不会阻塞另一个执行这个方法的线程呢?为了验证我的想法写了一个简单的例子:

class Program{static Mutex mutex = new Mutex();static void Main(){//非线程池线程调用
            Method();//创建10个定时器(线程池线程)for (int i = 0; i < 10; i++){System.Timers.Timer timer = new System.Timers.Timer();timer.Elapsed += new ElapsedEventHandler(TimerCallBack);timer.Interval = 2000;timer.Enabled = true;}Console.Read();}//定时器事件处理程序private static void TimerCallBack(object obj, EventArgs agrs){Method();}private static void Method(){try{mutex.WaitOne();Console.WriteLine("TimerCallBack:" + DateTime.Now + "Thread ID:"+ Thread.CurrentThread.GetHashCode() + " IsThreadPoolThread:" + Thread.CurrentThread.IsThreadPoolThread);//throw new Exception();
                mutex.ReleaseMutex();}catch (Exception){return;}}}

这个例子很简单在Main方法里直接执行Method方法和启动10个定时器去回掉这个方法,前者是在主线程中执行Method后者则是在线程池线程中执行,Method方法里打印出当前执行线程的一些信息。并且在输出信息前利用一个全局的静态的Mutex去阻塞线程,在输出信息之后再释放这个互斥体。正常的情况下不会有什么问题。

但是如果去掉//throw new Exception();的注释并且糟糕的是这里吞噬掉了异常你会发先整个线程池的线程无声无息的被阻塞了。其实解决方法很简单将mutex.ReleaseMutex()这行放入finally块就可以了。说实话我在心底是有点想骂写这个代码人的冲动啊,但心想终于找到原因所在了也就释怀了。但真的只是写这个代码的人的错吗?

转载于:https://www.cnblogs.com/zhanjindong/archive/2012/12/27/2835040.html

感谢你遇到的问题(2)相关推荐

  1. 通用权限管理组件使用说明书V3.0 错误校正 感谢自由软件职业者Helper(767870484)...

    有时候,真想做个像样的东西出来,但是往往各方面的能力都不够,这么多人,Helper(767870484)仔细认真的阅读了这个帮助手册.并给给于了指正,在这里非常感谢,你的劳动成果已经被通用权限管理积累 ...

  2. 中科大倪茹:感谢开源,我从入门竞赛到Top 10的经验分享

    个人介绍 给大家介绍下自己吧,个人信息.个人社交(github.知乎.csdn)地址.个人经历.竞赛经历 大家好鸭,我叫倪茹,是中国科学技术大学控制工程专业研二学生,Coggle小组成员,研究方向是人 ...

  3. 首位猪心移植患者去世!术后存活2个月,创造医学里程碑;官方讣告:哀悼并感谢所作巨大贡献...

    点击上方"视学算法",选择加"星标"或"置顶" 重磅干货,第一时间送达 金磊 丰色 发自 凹非寺 量子位 | 公众号 QbitAI 成功移植 ...

  4. 任正非:要感谢特朗普,他一吓唬,治好了华为人的富裕病,都努力工作了

    华为"心声社区"前几日公布了任正非接受北欧媒体采访的纪要.据纪要显示,前几日任正非接受采访,参与采访的记者来自瑞典国家电视台.丹麦广播公司等多家媒体.任正非在采访中就华为建筑的欧式 ...

  5. 黑客提交漏洞先获感谢后被举报 网络安全行业或现标志性事件

    在白帽子们看来,这就是一次普通得不能再普通的找漏洞行为:在世纪佳缘公司看来,这是在保护用户数据上做了一个正常的决定.但是当这两者碰到一起,就演变成了白帽子圈子里不亚于一场地震的抓人事件.谁对谁错?现在 ...

  6. java感谢_这三天看完Java入门第一季和第二季的成果!纪念一下!感谢Java入门的作者!...

    //main入口 package RentCar; import java.util.Scanner; public class Dada { public static void main(Stri ...

  7. 07-图6 旅游规划 (25分)(以此感谢zyx佬)

    这个题的话算是模板题改编了一点吧,不过个人感觉这个改编很有助于你理解迪杰斯特拉这个算法的真谛. 题解:新开一个cost数组来记录花费,仍然是用了优先队列优化的一个思想,与模板题不同的是只需要加一句话( ...

  8. 阿里某员工论坛炫耀:感谢公司让毕业不到两年的我年入百万

    一名标签为阿里巴巴的网友在匿名社区公然炫耀,称刚刚聊完绩效,3.75,年终奖高的吓到我了,感谢阿里让毕业不到两年的我年薪百万.当然,这部分奖金也包含股票收入. 对于年终奖话题,自然很快就吸引了众多同行 ...

  9. 金山员工被离职后拿到高薪工作:感谢公司辞退我,还给我赔偿金

    金山办公,是小米创始人雷军所在的前公司,目前仍然和雷军有着千丝万缕的联系.近日,一名标签为金山的网友称自己被公司离职:来还愿,感谢金山办公裁了我,顺利拿到大礼包,一个月的假期和赔偿金,某大厂涨薪50% ...

  10. 中国数学界,无论怎样感谢哈代都不为过

    // 同画家和诗人的模式一样,数学家的模式必然是美的.与色彩和文字相同,思想也必然会以某种和谐的方式组合.美是首要的试金石:丑陋的数学不可能永存. --戈弗雷·哈代(Godfrey Harold Ha ...

最新文章

  1. Python基础概念_6_模块
  2. 万能点位图软件_【像素图】复古提花毛衣+秋风落叶十字绣图,非常适合这个季节...
  3. SAP Cloud for Customer里Sales Order和Sales Quote的建模方式
  4. python浅拷贝的说法_Python中List的复制(直接复制、浅拷贝、深拷贝)
  5. 关于c#中的string
  6. xpose 调试支付宝
  7. SPSS 产生正交数据集
  8. “大劈棺”与“小手段”
  9. 排列组合—— 球盒问题
  10. Unity 实现2D地面挖洞!涂抹地形(碰撞部分,方法一)
  11. “爱心银行”让爱心增值(转自中国文明网)
  12. 【MVC 4】4.MVC 基本工具(Visual Studio 的单元测试、使用Moq)
  13. linux中rabbitmq服务启动失败,linux系统RabbitMQ启动错误记录
  14. 微型计算机nuc 6i5syk,Intel 英特尔 NUC Kit NUC6i5SYH 紧凑型准系统 开箱(附让人崩溃的系统问题)...
  15. 新晋院士,任大学校长!
  16. 仿购物商城-多级菜单搜索-搜索联动
  17. 牛客每日练习----调皮的孩纸,删除子串,哲哲的疑惑
  18. 学海无涯!如何在Android-Studio下进行NDK开发,全网疯传
  19. 树莓派连接温度传感器实时监控,并上报服务器
  20. 自由天空XP/2K3封装工具 Easy Sysprep v2.0 正式版封装教程

热门文章

  1. 关于嵌入式学习随笔-1《STM32简介》
  2. ROS入门(一) 文件结构篇
  3. 三角网格表面高斯曲率的计算与可视化
  4. springboot10-springcloud-eureka 服务注册与发现,负载均衡客户端(ribbon,feign)调用
  5. 用C#来开发CAD插件,含源代码
  6. Mac OS X上安装 Ruby运行环境
  7. android禁止锁屏保持常亮
  8. 网上的一个PHP分页函数,测试可用
  9. MySql Odbc等驱动下载地址分享下
  10. VS2008+QT+CYAPI开发USB程序问题