C++多线程面向对象解决方案

来源:互联网 发布:淘宝代购手办是正版么 编辑:程序博客网 时间:2024/05/17 03:20

相信很多人都读过《C++沉思录》这本经典著作,在我艰难地读完整本书后,留给我印象最深的只有一句话::“用类表示概念,用类解决问题”。

关于多线程编程,如果不是特别需要,大多数开发人员都不会特意去触碰这个似乎神秘的领域。如果在某些场合能正确并灵活地运用,多线程带来的好处是不言而喻的。然而,任何事物都有两面性,如果程序中引入多线程,那么我们需要谨慎小心地处理许多与之相关的问题,其中最突出的就是:资源竞争、死锁和无限延迟。那么面向对象与这些有什么关系了吗?有,面向对象的基础是封装,是的,正是封装,可以很好的解决多线程环境中的主要困境。

一.多线程环境

在开始之前,有必要先重温一下多线程环境。所谓,多线程编程,指的是在一个进程中有多个线程同时运行,每一个线程都有自己的堆栈,但是这些线程共享所有的全局变量和资源。

在引入互斥量之前,多线程的主要问题是资源竞争,所谓资源竞争就是两个或两个以上的线程在同一时间访问同一资源。为了解决这个问题,我们引入了互斥量,以同步多个线程对同一资源的访问。

然而在引入互斥量之后,新的问题又来了。因为如果互斥量的获得和释放没有得到正确处理,就会引起严重的问题。比如,某线程获得互斥量后,就可以对受保护的资源进行访问,但是如果访问完毕后,忘记了释放互斥量,那么其它的线程永远也无法访问那个受保护的资源了。这是一种较简单的情况,还有一种复杂的,那就是线程1已经拥有了资源A,但是它要拥有资源B后才能释放A,而线程2了恰好相反,线程2已经拥有了资源B但是它要拥有资源A后才能释放B。这样一来,线程1和线程2就永远地相互等待,这就是所谓的死锁。

死锁导致的问题是严重的,因为它使得程序无法正常运行下去。也许引入一些规范或约束有助于减少死锁发生的几率,比如我们可以要求,所有资源的访客(客户,使用者)都必须在真正需要资源的时刻请求互斥量,而当资源一使用完毕,就立即释放它,另外,锁定与释放一定要是成对的。如果上面的线程1和线程2都遵守这个规范,那么上述的那种死锁情况就不会发生了。

然而,规范永远只是规范,规范能被执行多少要依赖于使用者的自觉程度有多高,这个世界上总是有对规范和约束视而不见的人存在。所以,我们希望能够强制执行类似的约束,在对使用者透明的情况下。对于约束的强制实施可以通过封装做到。

 

多线程面向对象解决方案

首先你需要将系统API封装成基础类,这样你就可以用面向对象的武器类对付多线程环境,二是将临界资源与对其的操作封装在一个类中。这两点的核心都是将问题集中在一个地方,防止它们泛滥在程序的各个地方。

 1.  将系统API封装成基础类。

 1.  将系统API封装成基础类。

厌倦了每次涉及共享资源操作时都需要调用InitializeCriticalSectionDeleteCriticalSectionEnterCriticalSectionLeaveCriticalSection,并且它们是成对使用的,如果你调用了EnterCriticalSection,却忘了调用LeaveCriticalSection,那么锁就永远得不到释放,并且这些API的使用是很不直观的。我喜欢将它们封装成类,只需封装一次,以后就不用再查MSDN,每个API怎么写的了,参数是什么,免去后顾之忧。而且,在类的构造函数中调用InitializeCriticalSection,析构函数中调用DeleteCriticalSection,可以防止资源泄漏。面向对象的封装真是个好东西,我们没有理由拒绝它。

来看看我封装的几个与多线程环境相关的基础类。

// CriticalSection类用于解决对临界资源的保护

class CriticalSection

{

protected:

       CRITICAL_SECTION critical_section ;

public:

       CriticalSection()

       {

              InitializeCriticalSection(&this->critical_section) ;

       }

       virtual ~CriticalSection()

       {

              DeleteCriticalSection(&this->critical_section) ;

       }

       void Lock()

       {

              EnterCriticalSection(&this->critical_section) ;

       }

       void Unlock()

       {

              LeaveCriticalSection(&this->critical_section) ;

       }

}; 

//Monitor用于解决线程之间的同步依赖

class Monitor

{

private:

HANDLE event_obj ;

public:

Monitor(BOOL isManual = FALSE)

{

        this->event_obj = CreateEvent(NULL ,FALSE ,isManual ,"NONAME") ;

}

~Monitor()

{

        //ReleaseEvent()

        CloseHandle(this->event_obj) ;

} 

void SetIt()

{

        // 如果为auto,则SetEventevent obj设为有信号,当一个等待线程release后,

        //event obj自动设为无信号

        //如果是manual,则release所有等待线程,且没有后面自动重设

        SetEvent(this->event_obj) ;

}

void ResetIt()

{    

        //手动将event obj设为无信号

        ResetEvent(this->event_obj) ;

} 

void PulseIt()

{

        // 如果为auto,则PulseEventevent obj设为有信号,当一个等待线程release后,

        //event obj自动设为无信号

        //如果是manualPulseEventevent obj设为有信号,且release所有等待线程,

        //然后将event obj自动设为无信号

        PulseEvent(this->event_obj) ;

} 

DWORD Wait(long timeout)

{

        return WaitForSingleObject(this->event_obj ,timeout) ;

}

}; 

//Thread是对线程的简单封装

class Thread

{

private:    

HANDLE threadHandle ; 

unsigned long  threadId ;    

    unsigned long  exitCode ;

BOOL needTerminate ;

 public:

 public:

Thread(unsigned long exit_code = 0 )

{           

        this->exitCode = exit_code ;

        this->needTerminate = FALSE ;

}

 ~Thread(void)

 ~Thread(void)

{

        if(this->needTerminate)

        {           

               TerminateThread(this->threadHandle ,this->exitCode) ;

        }

} 

long GetTheThreadID()

{

        return this->threadId ;

} 

void Start(FunPtr pfn ,void* pPara)//启动线程

{

        this->threadHandle = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)(pfn) ,pPara ,0,&(this->threadId));  

}

void SetTerminateSymbol(BOOL need_Terminate)

{

        this->needTerminate = need_Terminate ;

} 

void wait(void)

{

        WaitForSingleObject(this->threadHandle,INFINITE) ; //用于阻塞宿主线程,使其不能早于本线程结束

}

}; 

        在大多数的多线程环境中,上述的几个类已经够用了,不如要实现更强劲的同步机制,你可以仿照上面自己进行封装。

2.  将临界资源与对其的操作封装在一个类中,如果这样做,锁的操作自动在类的实现中完成,而外部使用者不用关心是否处在多线程环境。也就是说这个类是线程安全的,在单线程和多线程环境下都可以使用。

比如我们经常需要使用线程安全的容器,我就自己封装了一个:

// SafeObjectList 线程安全的容器

#include <list>

#include "../Threading/CriticalSection.h"

 

plate<class T> class SafeObjectList : CriticalSection

{

private:

list<T> inner_list ;

list<T>::iterator itr ;

public:

void Add(T obj)

{

        this->Lock() ;

        this->inner_list.push_back(obj) ;

        this->Unlock() ;

}

void Remove(T obj)

{

       

        this->Lock() ;

        for(this->itr = this->inner_list.begin() ;this->itr != this->inner_list.end() ;this->itr++)

        {

               if(obj == (*(this->itr)))

               {

                      this->inner_list.erase(this->itr) ;

                      break ;

               }

        }

        this->Unlock() ;

       

} 

void Clear()

{

        this->Lock() ;

        this->inner_list.clear() ;

        this->Unlock() ;

}

int Count()

{

        return (int)this->inner_list.size() ;

} 

BOOL Contains(T& target)

{

        BOOL found = FALSE ; 

        this->Lock() ;

        for(this->itr = this->inner_list.begin() ;this->itr != this->inner_list.end() ;this->itr++)

        {

               if(target == (*(this->itr)))

               {

                      found = TRUE ;

                      break ;

               }

        }

        this->Unlock() ; 

        return found ; 

} 

BOOL GetElement(int index ,T& result)

{

        BOOL succeed = FALSE  ;

        this->Lock() ;       

        if(index < (int)this->inner_list.size())

        {

               int i= 0 ;

               for(this->itr = this->inner_list.begin() ;this->itr != this->inner_list.end() ;this->itr++)

               {

                      if(i == index)

                      {

                             result =  (*this->itr) ;

                             break ;

                      }

                      i++ ;

               }    

              

               succeed = TRUE ;

        }

        this->Unlock() ;

       

        return succeed ;           

}    

};

 

       在将临界资源与对其的操作封装在一个类中的时候,我们特别要需要注意的一点是封装的线程安全的方法(函数)的粒度,粒度太大则难以复用,粒度太小,则可能会导致锁的嵌套。所以在封装的时候,一定要根据你的具体应用,视情况而定。我的经验是这样的,首先可以把粒度定小一点,但是一旦发现有锁的嵌套出现,就加大粒度,把这两个嵌套合并成一个稍微粗一点粒度的方法。