浅谈Windows多线程编程几个常见问题

来源:互联网 发布:狗爹域名 编辑:程序博客网 时间:2024/05/02 00:26
 

本文不讨论CriticalSection, Mutex, Semaphore, Event的如何使用,这里只谈在多线程容易碰到的问题.

1.函数重入
关于函数重入的概念我这里就不多讲了.一般具有可重入性的函数暗含无状态特征,即函数的输出只由当前函数的输入决定而不依赖其他状态,
多线程环境中,如果要防止函数重入,就要防止函数每次调用之间的数据状态依赖.比如如果你的函数修改静态的,全局的或类的成员变量时,就要注意了,可能你的每次函数调用,会对下一次调用状态产生影响.函数的局部变量修改则重入不会产生问题.
举个例子,比如有个class的函数对成员进行修改,可能多个线程对同一对象进行调用:
void CTest::Test()
{
    this->m_bStarted = FALSE;
    // Section 1....
    this->m_bStarted = TRUE;

    if (this->m_bStarted)
    {
      //Section 2....
    }
}
如果两个线程同时调用该函数,函数将不按照预期的行为执行,可能另一线程在你把m_bStarted置为TRUE后,再次进入把m_bStarted置为FALSE,本来你要执行Section 2的代码结果没执行.

2.工作线程
一个对象为了设计需要,比如Services对象,可能需要创建新的工作线,并且新建线程会访问对象的成员变量.这时要特别注意对象的销毁,否则很容易访问非法内存.因为存在这种情况,使用者把该对象delete后,并没有通知线程结束,或者先于线程销毁了对象,线程可能继续运行,并且访问该实例对象,导致非法内存访问(Access violation).

3.跨线程指针传递
这个问题不限于多线程情况,但是在多线程情况下容易发生,一个全局或者参数指针,经过多次传递,到另一个线程使用.但是指针所指对象销毁时,没有或者没办法通知到所有使用该对象的线程,也可能导致非法内存访问.一种解决方法是加Ref Counter,还有智能指针什么的,让对象通过Ref来自己管理生命周期.

4.状态同步
经常我们一个成员函数经常基于前一函数调用状态,有顺序依赖,通常如果在单线程情况下,不会有问题,但在多线程情况下,程序写得不好,经常产生问题.比如,写一个Service类,可能有下面一些methods, Startup, Work, Shutdown,现在假设只有Startup成功才能开始work,只有work的对象才需要Shutdown.
如果单线程下,你按照一定调用顺序不会有问题,
pService->Startup();
pService->Work();
pService->Shutdown();
如果是多线程,就不一定了,说不定你Startup还没完成,另一个thread正在Shutdown,肯定不是你想要的预期行为.这时你必须加一些flag去确保只有Startuped的对象才能Work,标识为Startuped或者已经worked状态的对象才需要Shutdown,类似状态机一样.
在多线程中,同步以及改变状态flag,你就要用到同步/互斥对象以及函数,例如CriticalSection, Mutex, Semaphore和Event,另外就是Interlocked**系列函数,有关Interlocked**函数,可以参照MSDN的Interlocked Variable Access.

5.死锁

实际应用中,在多线程中使用回调事件通知机制,比较容易发生死锁,比如主线程已经在处理中locked A,同时回调事件通知线程也在进行处理事件locked B,当主线程访问进行处理时,就可能会等待lock B,而回调线程此时有可能会通知主线程某些事件发生,hold住lock B, 并等待lock A,死锁发生.

线程#1在获得Lock A后,需要获得Lock B,而同时,线程#2在Lock B后,需要获得Lock A。对于线程#1和#2,由于都不能获得满足的条件,就形成死锁了。


记住死锁发生的条件:Mutual exclusion, Hold and wait,No pre-emption,Circular wait.简单点就是,在使用互斥资源时,避免多个线程形成环式等待,或者一次性把所有资源全请求好,当线程所需资源不能全部获得时,最好把已占用资源释放,锁不是加的越多越好,只在需要发生资源竞争情况下使用,并且减少加锁范围, 能在被调用函数中加锁的话,就不要在调用函数中加锁,重复加锁不能使你的程序更加安全,只会增加死锁的可能, 只有member,static,global,shared data的访问和修改需要互斥,对于通过参数传递的数据上层应保证数据的同步访问,这样才能划分同步职责。