1.JTHREAD介绍
实际项目中经常会涉及到多线程架构。为了给WINUX(Windows+Linux)平台提供一套相同的操作线程的接口,需要将平台上对线程操作的API封装成一个的通用类。JTHREAD即是这样的一个开源类库。
JTHREAD是很简单的,主要包含JThread类和JMutex类,它们分别代表一个线程和一个互斥体,互斥体是为了同步多线程通信。该开发包作了简单的跨平台实现,对于*NIX平台调用pthread库,对于Windows平台调用Win32 Theads库。
本文基于JTHREAD库源码做简单剖析,以对前面线程控制、线程同步等议题实做演练。
按照惯例,对于返回int类型值的函数,若返回大于等于0的值表示成功,负值表示出错。
2.JMutex
2.1 JMutex
下面是类JMutex的定义。
其数据成员根据平台区分。对于Windows系统(Win32或WinCE),若定义JMUTEX_CRITICALSECTION宏,则使用临界区对象CRITICAL_SECTION作为互斥体;否则,使用互斥内核对象(Mutex)作为互斥体。对于*NIX系统使用pthread_mutex_t作为互斥体。后面主要针对Windows系统解说,鉴于临界区旋转锁的高效性,建议使用JMUTEX_CRITICALSECTION。
int Init()完成互斥体的初始化;bool initialized跟踪初始化记录,确保只初始化一次。可调用bool IsInitialized()获取initialized的值,以判断是否已经初始化。
在int Init()中,如果定义了JMUTEX_CRITICALSECTION宏,则调用InitializeCriticalSection(&mutex)初始化临界区;否则mutex = CreateMutex(NULL,FALSE,NULL),创建互斥内核对象。
Lock()/Unlock()为同步加解锁操作。Lock()内部体现为EnterCriticalSection(&mutex)进入临界区,或WaitForSingleObject(mutex,INFINITE)返回,可拥有互斥对象。Unlock()内部体现为LeaveCriticalSection(&mutex)离开临界区,或ReleaseMutex(mutex)释放拥有的互斥对象。
构造函数JMutex()中初始化initialized = false;析构函数~JMutex()中DeleteCriticalSection(&mutex)删除临界区或CloseHandle(mutex)关闭互斥内核对象,释放互斥体资源。
在你使用一个JMutex类的实例对象之前,你首先必须调用Init()函数执行初始化。通过检测IsInitialized()的返回值可以检测互斥体是否已经初始化。初始化之后,通过调用Lock()和Unlock()封闭需要同步的共享资源操作代码段。
2.2 JMutexAutoLock
下面是类JMutexAutoLock的定义。
JMutexAutoLock需要传入一个JMutex对象的引用来构造,当然,要求该JMutex对象已经初始化。在构造函数中调用mutex.Lock()上锁,在析构函数中mutex.Unlock()解锁。
JMutexAutoLock即所谓的自动锁,是对JMutex的自动管理,同步对象声明到对象生命周期结束之间的代码段(块)。它更容易实现线程安全,不用去担心什么时候为互斥体解锁。
2.3 JMutex示例
(1)在fun1()中,mutex.Lock();和mutex.Unlock();之间没有任何代码,则此处只是等待外部使用mutex保护的代码块运行完毕。如果外部占用该mutex,则此处等待;如果外部已释放该mutex,则此处继续执行DoSomeWork()。这里纯粹是等待外部事件发生:后上锁的等待先上锁的解锁。
(2)fun2()和fun3()是等价的,都是为了保护DoSomeWork()过程中涉及到的共享资源操作。
(3)在fun3()中,如果DoSomeWork()中途异常exit,则AutoLock不能正确析构,永远不会解锁。在此等待的后续线程(如果没有当掉)死锁。
3.JThread
下面是JThread类的定义。
3.1 JThread成员
threadid为线程ID号,threadhandle为线程内核对象句柄。
JThread类拥有三个JMutex对象成员,runningmutex、continuemutex和continuemutex2。bool mutexinit为三个对象初始化状态记录,只有三个互斥对象都成功初始化,才能协作完成后续对线程流程的正确控制。
TheThread(void *param)为通常意义上的线程入口函数,传递一个void*指针作为线程参数,该类静态入口传递JThread线程对象this指针。TheThread中调用Thread()完成特定的任务。我们姑且称TheThread()为线程壳(Shell),Thread()为线程核(Core)。
bool running为线程运行状态;bool IsRunning()为对该状态属性的访问。
void* retval为线程函数运行结果;void *GetReturnValue()为对该属性的访问。
3.2 JThread类剖析
所谓同步是指多线程之间的同步,同一线程内部顺序执行不存在同步问题。JThread类中runningmutex、continuemutex、continuemutex2主要为了与它的创建线程同步。它的创建线程就是MyJThread对象实例声明代码所在的线程,也即调用JThread::Start()的父线程。注意线程壳TheThread()是为线程入口函数,其中调用MyJThread对象实例(this)的Thread(),线程壳TheThread()和线程核Thread()代码运行于新线程。因此,在Start()和TheThread()/Thread()间存在过程状态控制的同步问题。
更一般的同步问题体现在MyJThread对象实例声明代码所在的线程与新建线程关于running状态及返回值retval的访问。runningmutex互斥体主要用来保护running状态变量的访问,当然retval的访问与running状态密切相关,只有运行完才有返回值。
下面结合具体代码来分析Start()->TheThread()->Thread()的过程控制。
continuemutex互斥体用来同步等待新线程调度,具体来说Start()中调用_beginthreadex创建新线程之前即上锁。然后,在runningmutex的保护下等待running被线程壳TheThread()置为true,Start()中的continuemutex才解锁,TheThread()继而执行线程核Thread()。
新线程一调度(Start),即进入线程壳TheThread(),continuemutex2互斥体即上锁;Start()中continuemutex解锁,但仍未返回,还需等待continuemutex2解锁。在线程核Thread()中需立即调用ThreadStarted()解锁continuemutex2,此时Start()返回。也即在实际进入线程核Thread()执行时,Start()才返回。
由以上分析可知:
- runningmutex:父线程等待新线程调度,新线程调度时会设置信号量Signal(running = true),即runningmutex互斥体用来保护running状态变量的同步读写。
- continuemutex:新线程等待父线程获知自己已调度运行,即continuemutex互斥体同步的是线程的创建到线程被调度(线程壳真正启动,running=true)过程。
- continuemutex2:父线程等待新线程核执行,即continuemutex2互斥体同步的是线程被调度到线程核真正执行过程(Thread()->ThreadStarted())。此时,父线程真正启动了新线程(而不仅仅是创建成功),Start()才返回。
3.3 JThread类的使用说明
因为含有未实现的纯虚函数virtual void *Thread() = 0,故JThread为抽象基类,无法直接声明创建JThread对象实例。在使用时,必须编写派生类实现Thread()接口,以完成特定的任务:classMyJThread : publicJThread。这样,一个MyJThread类实际上只能完成一种特定的任务。如以上代码所述,往往为线程壳TheThread()传递JThread对象的this指针,以便线程核Thread()能访问派生类实例对象属性。记得在你自己的Thread()实现中实时调用ThreadStarted()使父线程Start返回。
一个MyJThread对象管理完成特定任务的一个线程对象,其行为具有不可重入性。意即当MyJThread::Start()中开辟一条线程,mutexinit、runningmutex、continuemutex、continuemutex2、running、retval等都是针对一次线程行为及状态的管理。如果在Start()没有返回之前,或者线程过程没有返回之前,试图再次调用该实例的Start()进行新线程的创建,则上述一套设施服务于两个线程对象,则容易造成管理上错乱。实际上JThread::Start()已经对运行状态作了检测,连续调用Start(),将导致ERR_JTHREAD_ALREADYRUNNING错误。当然Start()后,确保运行结束,可再次Start()开辟新的线程,以完成同类多任务,但此时已经丧失了多线程并发的初衷,因为实际上这里是一个线程跑完,才开另一个线程。
对于同类多任务,往往声明创建多个MyJThread对象实例,然后Start()。在理想情况下,让线程核Thread()、线程壳TheThread()自然返回,以使其寿终正寝。迫不得已,可调用Kill()杀死线程。Kill()调用的是线程终结者TerminateThread(),如前所述,这种粗暴的行径将导致不良的后果,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈。当然,在继承的MyJThread,往往需要改进Kill()操作,以便作更优雅的控制退出。
记住,一个MyJThread对象对应一个线程对象,你每Start一个MyJThread实例,就相当于创建一个线程。当然,只要你的MyJThread扩展到足够强壮,你也可以将同一级别的不同类任务在Thread()中作统一处理,这取决于你的业务分工强度。
Start()理应让Thread()自然返回,Start()后通过调用IsRunning()函数可以检测线程是否在运行;若运行完毕(running=false),则可通过调用GetReturnValue()函数可以获取返回值。最后,你可以通过Kill()函数中止一个正在运行的线程。对于一个已经返回的线程,Kill()调用返回ERR_JTHREAD_NOTRUNNING。
3.4 JThread应用实例:JRTPLIB中的RTPPollThread
JTRPLIB中的RTP会话类RTPSession包含一个RTPPollThread* pollthread成员。RTPPollThread为JRTPLIB中的RTP会话响应线程,继承自JThread类。
RTPPollThread::Start()重载了基类的同名函数,作特定的初始化,调用JThread::Start()。RTPPollThread::Stop()对JThread::Kill()进行了安全扩展,如果等5秒后依旧JThread::IsRunning(),才调用JThread::Kill()强制关闭。
如果定义了RTP_SUPPORT_THREAD宏,则RTPSession支持多线程响应会话,usepollthread = true。在RTPSession::Create()中调用RTPSession::InternalCreate(),其中中创建线程(对象)。
RTPPollThread::Thread()线程核处理具体的RTCP/RTP通信会话。在RTPSession::ProcessPolledData中调用RTPSessionSources::ProcessRawPacket。RTPSessionSources::ProcessRawPacket中判断包的类型是RTCP还是RTP,若是RTCP包,则ProcessRTCPCompoundPacketàOnRTCPCompoundPacket处理;若是RTP包,则ProcessRTPPacketàOnRTPPacket处理,从而完成RTCP/RTP通信。
关于JRTPLIB的使用,参考《JRTPLIB@Conference DIY视频会议系统》。
4.CWinThread简介
CWinThread作为MFC的线程管理类,极具参考性,源码参阅Microsoft Visual Studio/VC98/MFC/SRC/THRDCORE.CPP。尽管其采用了面向对象的封装,但在实作时,通常按照_beginthreadex的方式调用AfxBeginThread传入线程函数地址和线程参数。内部对CWinThread对象作了自动化的管理。重点关注_AFX_THREAD_STARTUP结构中的hEvent和hEvent2是怎么样同步实现线程控制的。
实际应用中,如果不需要过于严格的封装需求,仅需对线程参数(ID、HANDLE、THREADPROC、THREADPARAM)等做简单的封装,以期控制。例如Peercast中的classThreadInfo。
5.参考