0,程序背景
公司开发一个delphi程序,执行数据采集和转发任务,作为硬件设备和主控软件之间的接口,起到软硬件分离的作用。
接口程序接收硬件的HTTP消息推送,分析处理后将数据存入MYSQL数据库,以及内存中。
主控软件定时与接口程序通信,接口程序将数据传给主控程序。主控程序分析后,将结果发给接口程序,接口程序将计算结果发送硬件上显示。
1, 死锁现象
接口程序采用了多线程,不定时发生程序无反应,即死锁状态。
死锁时,内存中的线程可能在20个以下,也有可能在接近1500个。内存和cpu消耗不大。
2, 故障分析
检查了临界区、同步函数等相关代码,排除以下隐患:
- 凡是多线程访问主线程界面显示资源,应加上锁。否则程序容易死掉。
- 应避免嵌套加锁、一个操作连续加多个锁。应做到用时再加锁,加锁马上用,用完即解锁。
- 检查程序中所有的EnterCriticalSection和LockList代码段。
但接口程序仍然不时死锁。
此时在代码中引入EurekaLog组件,对各类错误进行跟踪定位。在多线程程序的Execute方法中执行以下代码,跟踪死锁信息:
NameThread('This is my thread ' + ClassName); SetEurekaLogStateInThread(0, True);
程序死锁时,Eureka会自动弹出信息窗,发现以下信息:
*Exception Thread: ID=7656; Parent=0; Priority=0 | |Class=; Name=MAIN | |DeadLock=1; Wait Chain=Blocked waiting for critical section owned by thread [ 2A10 / 10768 ] -> thread -> critical section -> thread: [ 2A10 / 10768 ] is blocked -> SendMessage -> thread: [ 1DE8 / 7656 ] is blocked| |Comment= ================ |Running Thread: ID=10768; Parent=7656; Priority=0 | |Class=Thread_comm485; Name=This is my thread Thread_comm485 (Uthread_comm485.Thread_comm485.Execute) | |DeadLock=1; Wait Chain=Blocked waiting for SendMessage owned by thread [ 1DE8 / 7656 ] -> thread -> SendMessage -> thread: [ 1DE8 / 7656 ] is blocked -> critical section -> thread: [ 2A10 / 10768 ] is blocked| |Comment=
明显主线程和子线程互相锁定了。可以看到,主线程在等待子线程持有的临界区锁,而子线程在等待向主线程SendMessage 的结果。
检查代码,并未发现有调用SendMessage 的地方,很是郁闷。后来某次将TMemo上显示日志的操作屏蔽后,接口程序运行正常。由此说明,在Memo控件上添加内容和SendMessage 相关。
在网上查了一下,果然有 类似结论,Memo的Lines实际上是TMemoStrings,而Add实际上调用的Insert,Insert内调用了一系列的SendMessage,而此时主线程已经卡死,因此子线程也卡死,子线程释放不了锁,因此主线程也将一直等待。如此构成死锁,程序失去反应。
【参考文章:死锁,死锁,令人呕血的死锁 http://blog.sina.com.cn/s/blog_54800f3f0100w5oj.html】
3, 解决方案
原因清楚后,解决起来就容易了。其实很简单,也是多线程编程早就推荐的做法,不要在子线程中访问主界面资源。因此,将原来在子线程中直接在Memo中添加日志之类函数,改为发送消息给主线程,由主线程统一更新。
这里要注意,子线程如何将要添加的字符串传给主线程呢?本人试过网上说过的一种做法,将字符串指针转为整形指针,但实际运行发现经常出现访问错误。子线程的字符串地址在主线程中并不能访问,因为主线程处理时,子线程可能已经释放。因此,建议采用类似全局消息队列的方式,串行访问,子线程添加,主线程显示后删除。
同时要注意数据库读写操作。应避免在子线程中进行数据库操作,但即使是通过发送消息将数据库 操作转向主线程了,也要注意要在数据库操作的代码段加上串行化保护,否则程序可能因为数据库操作失去响应。