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

1JTHREAD介绍

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

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

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

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

 

2JMutex

2.1 JMutex

下面是类JMutex的定义。

class JMutex { public: JMutex(); ~JMutex(); int Init(); int Lock(); int Unlock(); bool IsInitialized() { return initialized; } private: #if (defined(WIN32) || defined(_WIN32_WCE)) #ifdef JMUTEX_CRITICALSECTION CRITICAL_SECTION mutex; // 临界区对象 #else // Use standard mutex HANDLE mutex; // 互斥内核对象 #endif // JMUTEX_CRITICALSECTION #else // pthread mutex pthread_mutex_t mutex; #endif // WIN32 bool initialized; };

        其数据成员根据平台区分。对于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的定义。

class JMutexAutoLock { public: JMutexAutoLock(JMutex &m) : mutex(m) { mutex.Lock(); } ~JMutexAutoLock() { mutex.Unlock(); } private: JMutex &mutex; };

JMutexAutoLock需要传入一个JMutex对象的引用来构造,当然,要求该JMutex对象已经初始化。构造函数中调用mutex.Lock()上锁,在析构函数中mutex.Unlock()解锁

JMutexAutoLock即所谓的自动锁,是对JMutex的自动管理,同步对象声明到对象生命周期结束之间的代码段(块)。它更容易实现线程安全,不用去担心什么时候为互斥体解锁。

2.3 JMutex示例

JMutex mutex; mutex.Init(); void DoSomeWork(); void fun1() { mutex.Lock(); mutex.Unlock(); DoSomeWork(); return; } void fun2() { mutex.Lock(); DoSomeWork(); mutex.Unlock(); return; } void fun3() { JMutexAutoLock AutoLock(mutex); DoSomeWork(); return; }

1)在fun1()中,mutex.Lock();mutex.Unlock();之间没有任何代码,则此处只是等待外部使用mutex保护的代码块运行完毕。如果外部占用该mutex,则此处等待;如果外部已释放该mutex,则此处继续执行DoSomeWork()这里纯粹是等待外部事件发生:后上锁的等待先上锁的解锁

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

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

 

3JThread

下面是JThread类的定义。

class JThread { public: JThread(); virtual ~JThread(); int Start(); int Kill(); virtual void *Thread() = 0; bool IsRunning(); void *GetReturnValue(); protected: void ThreadStarted(); private: #if (defined(WIN32) || defined(_WIN32_WCE)) #ifdef _WIN32_WCE DWORD threadid; static DWORD WINAPI TheThread(void *param); #else UINT threadid; static UINT __stdcall TheThread(void *param); #endif // _WIN32_WCE HANDLE threadhandle; // #else // pthread type threads pthread_t threadid; static void *TheThread(void *param); #endif // WIN32 void *retval; bool running; JMutex runningmutex; JMutex continuemutex, continuemutex2; bool mutexinit; };

3.1JThread成员

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.2JThread类剖析

所谓同步是指多线程之间的同步,同一线程内部顺序执行不存在同步问题。JThread类中runningmutexcontinuemutexcontinuemutex2主要为了与它的创建线程同步。它的创建线程就是MyJThread对象实例声明代码所在的线程,也即调用JThread::Start()的线程注意线程壳TheThread()是为线程入口函数,其中调用MyJThread对象实例(this)的Thread()线程壳TheThread()和线程核Thread()代码运行于线程。因此,在Start()TheThread()/Thread()间存在过程状态控制的同步问题。

更一般的同步问题体现在MyJThread对象实例声明代码所在的线程与新建线程关于running状态及返回值retval的访问。runningmutex互斥体主要用来保护running状态变量的访问,当然retval的访问与running状态密切相关,只有运行完才有返回值。

下面结合具体代码来分析Start()->TheThread()->Thread()的过程控制。

int JThread::Start() { if (!mutexinit) { // 初始化3个JMutex(代码略) } runningmutex.Lock(); if (running) { runningmutex.Unlock(); return ERR_JTHREAD_ALREADYRUNNING; } runningmutex.Unlock(); continuemutex.Lock(); #ifndef _WIN32_WCE threadhandle = (HANDLE)_beginthreadex(NULL,0,TheThread,this,0,&threadid); #else threadhandle = CreateThread(NULL,0,TheThread,this,0,&threadid); #endif // _WIN32_WCE if (threadhandle == NULL) { continuemutex.Unlock(); return ERR_JTHREAD_CANTSTARTTHREAD; } /* Wait until 'running' is set */ runningmutex.Lock(); while (!running) { runningmutex.Unlock(); Sleep(1); runningmutex.Lock(); } runningmutex.Unlock(); continuemutex.Unlock(); continuemutex2.Lock(); continuemutex2.Unlock(); return 0; } #ifndef _WIN32_WCE UINT __stdcall JThread::TheThread(void *param) #else DWORD WINAPI JThread::TheThread(void *param) #endif // _WIN32_WCE { JThread *jthread; void *ret; jthread = (JThread *)param; // this jthread->continuemutex2.Lock(); jthread->runningmutex.Lock(); jthread->running = true; // Thread is ready to run jthread->runningmutex.Unlock(); jthread->continuemutex.Lock(); jthread->continuemutex.Unlock(); ret = jthread->Thread(); jthread->runningmutex.Lock(); jthread->running = false; // Thread run over jthread->retval = ret; CloseHandle(jthread->threadhandle); // Close Handle jthread->runningmutex.Unlock(); return 0; } // Your own Thread implementation should call ThreadStarted immediately void JThread::ThreadStarted() { continuemutex2.Unlock(); }

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.3JThread类的使用说明

因为含有未实现的纯虚函数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.4JThread应用实例:JRTPLIB中的RTPPollThread

JTRPLIB中的RTP会话类RTPSession包含一个RTPPollThreadpollthread成员。RTPPollThreadJRTPLIB中的RTP会话响应线程,继承自JThread类。

class RTPPollThread : private JThread { public: RTPPollThread(RTPSession &session, RTCPScheduler &rtcpsched); ~RTPPollThread(); int Start(RTPTransmitter *trans); void Stop(); private: void *Thread(); bool stop; JMutex stopmutex; RTPTransmitter *transmitter; RTPSession &rtpsession; RTCPScheduler &rtcpsched; }; // 需要覆写实现的线程核 void *RTPPollThread::Thread() { JThread::ThreadStarted(); bool stopthread; stopmutex.Lock(); stopthread = stop; stopmutex.Unlock(); while (!stopthread) { int status; rtpsession.schedmutex.Lock(); rtpsession.sourcesmutex.Lock(); RTPTime rtcpdelay = rtcpsched.GetTransmissionDelay(); rtpsession.sourcesmutex.Unlock(); rtpsession.schedmutex.Unlock(); if ((status = transmitter->WaitForIncomingData(rtcpdelay)) < 0) { stopthread = true; rtpsession.OnPollThreadError(status); } else { if ((status = transmitter->Poll()) < 0) { stopthread = true; rtpsession.OnPollThreadError(status); } else { if ((status = rtpsession.ProcessPolledData()) < 0) { stopthread = true; rtpsession.OnPollThreadError(status); } else { rtpsession.OnPollThreadStep(); stopmutex.Lock(); stopthread = stop; stopmutex.Unlock(); } } } } return 0; }

RTPPollThread::Start()重载了基类的同名函数,作特定的初始化,调用JThread::Start()RTPPollThread::Stop()JThread::Kill()进行了安全扩展,如果等5秒后依旧JThread::IsRunning(),才调用JThread::Kill()强制关闭。

如果定义了RTP_SUPPORT_THREAD宏,RTPSession支持多线程响应会话usepollthread = true。在RTPSession::Create()中调用RTPSession::InternalCreate(),其中中创建线程(对象)。

int RTPSession::InternalCreate(const RTPSessionParams &sessparams); { // Do thread stuff if necessary if (usingpollthread) { // …… pollthread = RTPNew(GetMemoryManager(),RTPMEM_TYPE_CLASS_RTPPOLLTHREAD) RTPPollThread(*this,rtcpsched); if (pollthread == 0) { // …… } if ((status = pollthread->Start(rtptrans)) < 0) { } } }

  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

// Peercast/core/common/sys.h #ifdef WIN32 typedef int (WINAPI *THREAD_FUNC)(ThreadInfo*); typedef unsigned int THREAD_HANDLE; #endif class ThreadInfo { public: ThreadInfo() { active = false; id = 0; func = NULL; data = NULL; } void shutdown() { active = false; // sys->waitThread(this); } volatile bool active; // 是否有效 THREAD_FUNC func; // 线程运行函数 int id; // 线程ID THREAD_HANDLE handle; // 线程句柄 void *data; // 线程参数 };

你可能感兴趣的:(thread,多线程,编程,kill,winapi)