Win32多线程编程(4) — JTHREAD剖析

1JTHREAD介绍

实际项目中经常会涉及到多线程架构。为了给WINUXWindows+Linux)平台提供一套相同的操作线程的接口,需要将平台上对线程操作的API封装成一个的通用类。JTHREAD即是这样的一个开源类库。

JTHREAD是很简单的,主要包含JThread类和JMutex类,它们分别代表一个线程和一个互斥体,互斥体是为了同步多线程通信。该开发包作了简单的跨平台实现,对于*NIX平台调用pthread库,对于Windows平台调用Win32 Theads库。

本文基于JTHREAD库源码做简单剖析,以对前面线程控制、线程同步等议题实做演练。

按照惯例,对于返回int类型值的函数,若返回大于等于0的值表示成功,负值表示出错。

 

2JMutex

2.1 JMutex

下面是类JMutex的定义。

       其数据成员根据平台区分。对于Windows系统(Win32WinCE),若定义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()这里纯粹是等待外部事件发生:后上锁的等待先上锁的解锁

2fun2()fun3()是等价的,都是为了保护DoSomeWork()过程中涉及到的共享资源操作。

3)在fun3()中,如果DoSomeWork()中途异常exit,则AutoLock不能正确析构,永远不会解锁。在此等待的后续线程(如果没有当掉)死锁。

 

3JThread

下面是JThread类的定义。

3.1 JThread成员

threadid为线程ID号,threadhandle为线程内核对象句柄。

JThread类拥有三个JMutex对象成员,runningmutexcontinuemutexcontinuemutex2bool 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类中runningmutexcontinuemutexcontinuemutex2主要为了与它的创建线程同步。它的创建线程就是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()置为trueStart()中的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()中开辟一条线程,mutexinitrunningmutexcontinuemutexcontinuemutex2runningretval等都是针对一次线程行为及状态的管理。如果在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包含一个RTPPollThreadpollthread成员。RTPPollThreadJRTPLIB中的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::ProcessRawPacketRTPSessionSources::ProcessRawPacket中判断包的类型是RTCP还是RTP,若是RTCP包,则ProcessRTCPCompoundPacketàOnRTCPCompoundPacket处理;若是RTP包,则ProcessRTPPacketàOnRTPPacket处理,从而完成RTCP/RTP通信。

关于JRTPLIB的使用,参考《JRTPLIB@Conference DIY视频会议系统》。

 

4CWinThread简介

CWinThread作为MFC的线程管理类,极具参考性,源码参阅Microsoft Visual Studio/VC98/MFC/SRC/THRDCORE.CPP。尽管其采用了面向对象的封装,但在实作时,通常按照_beginthreadex的方式调用AfxBeginThread传入线程函数地址和线程参数。内部对CWinThread对象作了自动化的管理。重点关注_AFX_THREAD_STARTUP结构中的hEventhEvent2是怎么样同步实现线程控制的。

实际应用中,如果不需要过于严格的封装需求,仅需对线程参数(IDHANDLETHREADPROCTHREADPARAM)等做简单的封装,以期控制。例如Peercast中的classThreadInfo


5.参考

对Pthread线程进行简单的类封装》 

类成员函数作为pthread_create函数参数


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值