您的位置:首页 > 编程语言 > C语言/C++

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

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

于多线程编程,如果不是特别需要,大多数开发人员都不会特意去触碰这个似乎神秘的领域。如果在某些场合能正确并灵活地运用,多线程带来的好处是不言而喻
的。然而,任何事物都有两面性,如果程序中引入多线程,那么我们需要谨慎小心地处理许多与之相关的问题,其中最突出的就是:资源竞争、死锁和无限延迟。那
么面向对象与这些有什么关系了吗?有,面向对象的基础是封装,是的,正是封装,可以很好的解决多线程环境中的主要困境。
一.多线程环境
在开始之前,有必要先重温一下多线程环境。所谓,多线程编程,指的是在一个进程中有多个线程同时运行,每一个线程都有自己的堆栈,但是这些线程共享所有的全局变量和资源。
在引入互斥量之前,多线程的主要问题是资源竞争,所谓资源竞争就是两个或两个以上的线程在同一时间访问同一资源。为了解决这个问题,我们引入了互斥量,以同步多个线程对同一资源的访问。

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

锁导致的问题是严重的,因为它使得程序无法正常运行下去。也许引入一些规范或约束有助于减少死锁发生的几率,比如我们可以要求,所有资源的访客(客户,使
用者)都必须在真正需要资源的时刻请求互斥量,而当资源一使用完毕,就立即释放它,另外,锁定与释放一定要是成对的。如果上面的线程1和线程2都遵守这个规范,那么上述的那种死锁情况就不会发生了。
然而,规范永远只是规范,规范能被执行多少要依赖于使用者的自觉程度有多高,这个世界上总是有对规范和约束视而不见的人存在。所以,我们希望能够强制执行类似的约束,在对使用者透明的情况下。对于约束的强制实施可以通过封装做到。

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

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

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

厌倦了每次涉及共享资源操作时都需要调用InitializeCriticalSection、DeleteCriticalSection、EnterCriticalSection、LeaveCriticalSection,并且它们是成对使用的,如果你调用了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,则SetEvent将event obj设为有信号,当一个等待线程release后,
//event obj自动设为无信号
//如果是manual,则release所有等待线程,且没有后面自动重设
SetEvent(this->event_obj) ;
}
void ResetIt()
{
//手动将event obj设为无信号
ResetEvent(this->event_obj) ;
}
void PulseIt()
{
// 如果为auto,则PulseEvent将event obj设为有信号,当一个等待线程release后,
//event obj自动设为无信号
//如果是manual,PulseEvent将event 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 ;
}
};


将临界资源与对其的操作封装在一个类中的时候,我们特别要需要注意的一点是封装的线程安全的方法(函数)的粒度,粒度太大则难以复用,粒度太小,则可能会
导致锁的嵌套。所以在封装的时候,一定要根据你的具体应用,视情况而定。我的经验是这样的,首先可以把粒度定小一点,但是一旦发现有锁的嵌套出现,就加大
粒度,把这两个嵌套合并成一个稍微粗一点粒度的方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: