危险的线程
来源:互联网 发布:java布尔是什么 编辑:程序博客网 时间:2024/04/28 07:58
示例代码
class WorkClass
{
public:
void Start()
{
CreateThread(...,ThreadFunc,this,...)
}
void Stop()
{
}
static LRESULT WINAPI ThreadFunc(void* pThis)
{
return (WorkClass*)(pThis)->WorkFunc();
}
LRESULT WorkFunc()
{
....
}
}
WorkClass aWork;
aWork.Start();
等待线程结束
以上代码很容易导致地址非法访问,如果在aWork对象被销毁的时候线程还没有结束的话。
所以,安全的做法应该是在对象析构的时候,保证所有运行于其上的线程都已经结束。
因此需要保存线程的句柄,并在析构函数里等待线程结束。
m_WorkThread=CreateThread();
~WorkClass()
{
WaitForSingleObject(m_WorkThread);
}
事件通知
在工作线程里,经常有一些事件需要通知外部的调用者,通常使用的回调函数(建议使用http://sigslot.sourceforge.net/)。
比如
LRESULT WorkFunc()
{
...
RaiseSomeEvent();
...
}
外部的调用者使用OnSomeEvent()来响应以上事件,但是我们最经常忘记的是,OnSomeEvent是运行在工作线程里的。这里有很多限制,比如在这里操作Windows UI是危险(应该是禁止)的,在WinForm以及MFC里甚至直接报错。不仅如此,还可能会导致对一些共享资源的并发访问,导致不可预料的问题。因此,最常用解决办法是,通过PostMessage来把任务切换到UI线程上(.NET的Invoke函数对此做了很方便的封装),这样就可以变成串行运行。
OnSomeEvent(param)
{
PostMessage(WM_XXX,param)
}
OnXXX(param)
{
}
COM套间模型
因为并发访问会带来很多问题,所以微软的COM提出了套间模型。具体的细节很复杂,简单的说就是自动的把并行的方法调用变成串行执行的。其原理就是使用代理(调用者持有的访问指针),并在内部创建一个隐藏窗口,然后使用上面的PostMessage模式把要执行的方法放入消息队列等待消息循环处理。因为消息循环是串行的,所以不管把外部的并行调用变成了串行的。
也正因为套间的存在,所以对于STA的COM对象的一个方法如果是阻塞执行的(也就是需要很长的时间才返回),那么就会把整个COM对象所在的套间的线程阻塞(很多时候就是主/UI线程)。这时候,即便是从工作线程里访问该COM对象,也会被阻塞。因为,前一个方法已经把消息循环阻塞了......
如果涉及到多线程,涉及到同步阻塞执行,就一定要仔细研究COM套间模型。
停止线程
不应该简单的使用TerminateThread函数把线程干掉,因为会导致很多问题,比如对象的析构就无法实现了。所以,除非不得以,不要TerminateThread,而最好是设置一个标志位或者信号量,让工作线程监测到后自行退出。
比如
Stop()
{
m_stoped=true;
}
LRESULT WorkFunc()
{
if(m_stoped)
return 0;
}
特别要注意,避免在Stop()方法里等待线程结束,否则很容易出现死锁。因为,很多时候我们是在工作线程的回调(事件处理)函数里调用Stop()方法的。比如
Stop()
{
m_stoped=true;
WaitForSingleObject(m_WorkThread);
}
OnXXX()
{
Stop();
}
因为OnXXX是在工作线程中运行的,并且调用了Stop,而Stop发出了结束标记之后在等待工作线程结束,结果就不返回了--死锁了。
为了安全起见,应该在主(UI)线程里调用Stop方法,这样的话,即便是在Stop方法里等待工作线程结束也不会死锁。不过,如果结束的时间过长,会把主线程阻塞,于是界面会有一段时间的无响应。搞不好,还是会导致死锁。比如在Stop的过程中,工作线程会触发一系列事件(回调函数)。
OnXXX()
{
SendMessage()
}
前面已经说了,OnXXX是在工作线程里调用的,而SendMessage一般是往主/UI线程发送消息的,属于跨线程消息。如果是同线程消息,SendMessage是不用等待的,直接执行返回。而对于跨线程消息,则要进入消息队列,等待处理。但是,前面我们已经看到了主线程已经被Stop方法给阻塞了,SendMessage根本就不会返回!于是,整个世界就静止了!这里,我们又得出一个结论,慎用SendMessage,特别是跨线程SendMessage。除非知道是在同一个线程里,否则建议还是用PostMessage比较好,这样就没有问题了。
总结一下就是,
a. 避免在Stop方法里等待工作线程结束,可以放到析构函数里或者单独提供方法,比如WaitForShutdown。
b. 避免在事件处理函数里使用SendMessage,建议用PostMessage。
c. 使用窗口消息循环来处理事件,可以把并行变成串行,减少资源并发访问的冲突。
与界面的同步
至此,世界似乎已经OK了......且慢,又遇到问题了。在工作线程完成了处理,并通知界面进行处理之前,这中间的一霎那,如果用户对界面进行了操作怎么办?比如,一个按钮有两个状态“开始”和“结束”。点击开始之后,文字变成了“结束”。但是,就在用户点击按钮“结束”的时候,工作线程已经完成处理,内部状态已经完成,理论上这时候的按钮文字应为“开始”的。但是,在中间转换的一霎那,就有问题了。
为了避免这个不同步的问题,我们需要在先通知发消息给界面进行更新,同时更新内部数据的状态,并且应该是在一个消息处理函数里完成,这样就可以保证界面和数据的一致了--用户的操作必须在下一个消息循环才能处理,不可能在更新内部数据和界面的中间插入。
首先从工作线程转到主线程
OnXXX()
{
PostMessage(ON_WM_XXXX)
}
主线程响应ON_WM_XXXX
OnWMXXX()
{
//更新内部数据
//更新界面,可以使用SendMessae(),因为同一个线程,不怕阻塞。
}
结束语
多核时代,我们需要更多的使用多线程。但是,目前的多线程编程模型,却是陷阱多多......
- 危险的线程
- 线程活跃性危险
- Vista延期的危险
- OLE2A的潜在危险
- Vista延期的危险
- 危险的API
- getchar隐藏的危险
- 最危险的域名?
- gh0st的危险代码
- 指针的危险
- 危险的职业
- 危险的NSAutoreleasePool
- 危险的CString类
- 危险的QQ
- 危险的静态变量
- 最迷人的危险
- 过早优化的危险
- 危险的森林里
- 如何实现两个数据库的同步
- dojo学习笔记(三)
- c#验证码
- mysql 日期转换问题
- VB.net 观察者模式
- 危险的线程
- 孙振耀撰文谈退休并畅谈人生- 等待
- U盘不显示盘符的解决办法
- LINUX crontab 命令
- 越来越觉得编程是在浪费青春
- ORA-01220故障
- 孙振耀撰文谈退休并畅谈人生- 入对行跟对人
- dojo学习笔记(四)
- 孙振耀撰文谈退休并畅谈人生- 选择