危险的线程

来源:互联网 发布: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(),因为同一个线程,不怕阻塞。

}

 

结束语

多核时代,我们需要更多的使用多线程。但是,目前的多线程编程模型,却是陷阱多多......

原创粉丝点击