线程类的具体实现 一、将接口和实现相分离 分离接口和实现是OO设计中的重要思想。它其实反映了现实世界中抽象和具象、一般和特殊的基本现象和规律。而我们,正是基于这种思想,来完成我们类库的跨平台特性。 基本上所有的OO开发环境都提供了支持接口和实现分离的机制。很多机制是基于编程语言本身的,如可继承性和多态性,而有些机制还基于体系结构特性,比如CORBA和COM。 我们把有关线程的具体实现封装到“实现类”里面,命名为TheadImpl。实际上ThreadImpl只是一个薄层封装,为上层Thread类提供一个统一线程API调用界面。 二、包装系统的API 虽然只是一个薄层封装,我们不得不做常常是让类库设计者最头疼的一件事情---统一操作系统API的调用界面。每种操作系统都提供API,常常都是相互风格迥异,这给统一包装API界面带来很多困难---虽然同时也增加了这项工作的艺术性。设计者不得不充分调研各种对象操作系统,了解每个API的调用规格,详细归纳,慎重取舍,以设计出良好的统一包装界面。在这里系统编程经验至关重要。 虽然本系列的文章顺序是先介绍线程类Thread的接口,再来讨论ThreadImpl的具体实现,这可能会给读者带来自上而下(Top down)的设计路线的印象。而实际上,和许多工作的实际过程一样,C++类库设计也不是简单的自上而下或者自下而上(Bottom up),而是一个PDCA循环(规划 Plan-执行 DO-检查 Check-措施 Action)。据我的个人经验,每个类库在第一版设计完成后都要经过重复数次的修改,以达到一个相对稳定的版本。 以上的经验之谈是希望读者能理解和大多数工作一样C++类库设计也不是一件一蹴而就的直线性工作。下面介绍我们的对象系统的线程API---我们的包装对象。 三、盘点POSIX与Windows的线程API 和其他POSIX系统API一样,POSIX线程的API在POSIX规范 IEEE 1003系列中被定义,目前的版本为2004年版。POSIX线程提供丰富的系统调用,不过我们的线程类只使用其中的一部分。如果读者手头没有IEEE的文档也没有关系,因为POSIX线程被广泛实现于各种Linux平台,绝大多数Linux系统的手册页(Manual page)都有相关的叙述。 创建线程: POSIX: int pthread_create ( pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg ); pthread_create()创建线程并立刻执行它。 Windows: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); CreateThread()允许在dwCreationFlags参数中指定线程是否以挂起状态被创建(CREATE_SUSPENDED标志)。另外CreateThread允许通过dwStackSize指定线程初始栈大小。 这两个函数都接受一个用户例程函数指针,作为线程运行的用户代码入口。 用户例程的形式定义: POSIX: void * start_routine(void * param); Windows: DWORD WINAPI ThreadProc(LPVOID lpParameter); 设置线程初始栈: Posix: 通过线程属性设定。 int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize); Windows: 在CreateThread()中指定。 判断线程死活/有效性 POSIX: IEEE 1003中并没有明确指定判断线程死活的API。不过很多人使用pthread_kill(): int pthread_kill(pthread_t thread, int sig); 通过发送信号0来测试线程的有效性。这样的做的依据可能来源于标准IEEE 1003关于pthread_kill()的一段注释: “Upon successful completion, the function shall return a value of zero. The pthread_kill ( ) function shall request that a signal be delivered to the specified thread. As in kill( ), if sig is zero, error checking shall be performed but no signal shall actually be sent.” 另外规范中对于pthread_kill()的返回值定义如下: [ESRCH] No thread could be found corresponding to that specified by the given thread ID. [EINVAL] The value of the sig argument is an invalid or unsupported signal number. 调用pthread_kill,发送信号0,检测pthread_kill的返回值。如果pthread_t型的ID指向一个有效并且执行中的线程,则pthread_kill应该返回0;如果是一个无效ID或者线程已经结束,则pthread_kill应该返回ESRCH。 这看起来是个不错的办法,可惜笔者在实践中发现,在某些POSIX实现(具体的说是Cygwin的某些版本)中,调用pthread_kill并传递一个无效的线程ID将直接导致指针操作异常而使程序异常结束。 笔者发现使用另一个API好像更为安全: int pthread_getschedparam( pthread_t thread, int *restrict policy, struct sched_param *restrict param ); pthread_getschedparam()有类似的调用语义规范: “RETURN VALUE If successful, the pthread_getschedparam( ) and pthread_setschedparam( ) functions shall return zero; otherwise, an error number shall be returned to indicate the error. The pthread_getschedparam() function may fail if: [ESRCH] The value specified by thread does not refer to an existing thread.” 所以,在我们的版本中,用pthread_getschedparam取代pthread_kill来测试线程死活。pthread_getschedparam的这一用途在笔者所测试的Cygwin和Linux版本中发挥正常。 Windows: DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds ); Windows编程的规范做法,是调用WaitForSingleObject,指定等待时间为0。如果返回值为WAIT_TIMEOUT则说明线程为有效,否则为无效线程HANDLE或者线程已经结束。 线程的睡眠函数: POSIX: unsigned sleep(unsigned seconds); int usleep(useconds_t useconds); 这两个API提供不同时间粒度的睡眠。我们可以把它们组合起来以实现毫秒级粒度的睡眠功能。另外某些老版本的Linux实现中把sleep()和usleep()解释为进程级别的睡眠API---这已经过时了。根据IEEE 1003 2004的说明: “The sleep( ) function shall cause the calling thread to be suspended from execution until either the number of realtime seconds specified by the argument seconds has elapsed or a signal is delivered to the calling thread and its action is to invoke a signal-catching function or to terminate the process.” usleep也有内似的说明。目前的流行Linux版本的sleep和usleep都是线程级别的。 Windows: VOID Sleep(DWORD dwMilliseconds); 强行结束线程: POSIX: int pthread_cancel(pthread_t thread); Windows: BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode); 程序员应该尽量避免调用强行结束线程的API,尤其是在C++程序设计中。我们将在后续的文章中解释其原因。 四、完成线程实现类的代码 下面给出POSIX线程实现类代码。代码中使用了Java风格的注释。 //---- ThreadImpl_Linux.h---- #ifndef THREAD_IMPL_LINUX_H #define THREAD_IMPL_LINUX_H #include <pthread.h> #include <unistd.h> class Thread; /** * Linux(POSIX)版本的线程实现类。 */ class ThreadImpl { public: static const int WAIT_INFINITE = -1; static const int INVALID_THREAD_ID = 0; static const size_t DEFAULT_STACK_SIZE = 0; static const int WAIT_TIME_SLICE = 10; public: /** * 线程实现类的内部结构体。包含线程的平台相关信息。 */ struct ThreadStruct { pthread_t tThread; ThreadStruct() : tThread(INVALID_THREAD_ID) { } ~ThreadStruct() { } }; public: typedef int (* THREAD_FUNCTION) (Thread *pParam); typedef void * (*POSIX_THREAD_ROUTINE) (void *pParam); public: /** * 创建线程。 * @param ts线程实现类的内部结构体型变量。 * @param thread_func 用户例程函数指针。 * @param cbStackSize 线程初始栈大小,字节单位。 * @param pThread 绑定的Thread对象指针。 * @return 调用成功返回true,失败返回false。 */ static bool CreateThread(ThreadStruct & ts, THREAD_FUNCTION thread_func, size_t cbStackSize, Thread * pThread) { pthread_attr_t *pAttr = NULL; pthread_attr_t attr; if(cbStackSize != DEFAULT_STACK_SIZE) { if(0 != pthread_attr_init(&attr)) { return false; } if(0 != pthread_attr_setstacksize(&attr, cbStackSize)) { pthread_attr_destroy(&attr); return false; } pAttr = &attr; } int iRes = pthread_create(&(ts.tThread), pAttr, (POSIX_THREAD_ROUTINE)thread_func, (void*)pThread); if(NULL != pAttr) { pthread_attr_destroy(&attr); } if(0 != iRes) { return false; } pthread_detach(ts.tThread); return true; } /** * 销毁线程实现类的内部结构体。 * @param ts 线程实现类的内部结构体型变量。 */ static void DestroyThread(ThreadStruct & ts) { } /** * 在指定时间内等待线程结束。 * @param ts 线程实现类的内部结构体。 * @param iTimeout 指定的等待时间。 * @return 线程在指定时间内结束的情况下返回true,否则返回false。 * 如果传入的线程结构体未关联到有效线程,则返回true。 */ static bool WaitForThreadEnd(const ThreadStruct & ts, int iTimeout) { int iDelta = WAIT_TIME_SLICE; int iTotal = iTimeout; if(iTimeout == WAIT_INFINITE) { // Cause to do unlimited loop. iDelta = 0; iTotal = 1; } for(int i=0; i<iTotal; i+=iDelta) { if(!IsAlive(ts)) { return true; } else { Sleep(WAIT_TIME_SLICE); } } return false; } /** * 强行结束线程。 * @param ts 线程实现类的内部结构体型变量。 */ static void TerminateThread(ThreadStruct & ts) { ::pthread_cancel(ts.tThread); } /** * 判断线程死活。 * @param ts 线程实现类的内部结构体型变量。 * @return 线程为有效则返回true,否则返回false。 */ static bool IsAlive(const ThreadStruct & ts) { int iPolicy; struct sched_param sp; int iRes = pthread_getschedparam(ts.tThread, &iPolicy, &sp); if(0 == iRes) { return true; } else { return false; } } /** * 获取线程ID。该ID在进程域内唯一。 * @param ts线程实现类的内部结构体型变量。 */ static int GetThreadId(const ThreadStruct & ts) { return (int) ts.tThread; } /** * 导致调用线程被挂起若干时间。 * @param iMs 线程挂起时间,毫秒单位。 */ static void Sleep(int iMs) { int iS = iMs / 1000; int iUs = (iMs % 1000) * 1000; if(iS > 0) { sleep(iS); } if(iUs > 0) { usleep(iUs); } return; } private: // 禁止构造函数。 ThreadImpl(); // 禁止拷贝构造函数。 ThreadImpl(const ThreadImpl &) throw(); // 禁止赋值操作符。 void operator=(const ThreadImpl &); }; // class ThreadImpl #endif // #ifndef ThreadImpl_LINUX_H //----EOF---- Windows的线程实现类的代码如下: //----ThreadImpl_WIN32.h---- #ifndef THREAD_IMPL_WIN32_H #define THREAD_IMPL_WIN32_H #include <Windows.h> class Thread; /** * Windows 32位平台版本的线程实现类。 */ class ThreadImpl { public: static const int WAIT_INFINITE = -1; static const int INVALID_THREAD_ID = 0; static const size_t DEFAULT_STACK_SIZE = 0; public: /** * 线程实现类的内部结构体。包含线程的平台相关信息。 */ struct ThreadStruct { HANDLE hThread; DWORD dwThreadId; ThreadStruct() : hThread(NULL), dwThreadId(INVALID_THREAD_ID) { } void Cleanup() { if(hThread != NULL) { CloseHandle(hThread); hThread = NULL; } } ~ThreadStruct() { Cleanup(); } }; public: typedef int (* THREAD_FUNCTION) (Thread *pParam); public: /** * 创建线程。 * @param ts线程实现类的内部结构体型变量。 * @param thread_func 用户例程函数指针。 * @param cbStackSize 线程初始栈大小,字节单位。 * @param pThread 绑定的Thread对象指针。 * @return 调用成功返回true,失败返回false。 */ static bool CreateThread(ThreadStruct & ts, THREAD_FUNCTION thread_func, size_t cbStackSize, Thread * pThread) { ts.hThread = ::CreateThread(NULL, (DWORD) cbStackSize, (LPTHREAD_START_ROUTINE) thread_func, (LPVOID) pThread, 0, &ts.dwThreadId); if(ts.hThread == NULL) { return false; } return true; } /** * 销毁线程实现类的内部结构体。 * @param ts 线程实现类的内部结构体型变量。 */ static void DestroyThread(ThreadStruct & ts) { ts.Cleanup(); } /** * 在指定时间内等待线程结束。 * @param ts 线程实现类的内部结构体。 * @param iTimeout 指定的等待时间。 * @return 线程在指定时间内结束的情况下返回true,否则返回false。 * 如果传入的线程结构体未关联到有效线程,则返回true。 */ static bool WaitForThreadEnd(const ThreadStruct & ts, int iTimeout) { DWORD dwTO = (DWORD) iTimeout; if(iTimeout == WAIT_INFINITE) { dwTO = INFINITE; } DWORD dwRes = WaitForSingleObject(ts.hThread, dwTO); if(dwRes == WAIT_TIMEOUT) { return false; } else { return true; } } /** * 强行结束线程。 * @param ts 线程实现类的内部结构体型变量。 */ static void TerminateThread(const ThreadStruct & ts) { ::TerminateThread(ts.hThread, -1); } /** * 判断线程死活。 * @param ts 线程实现类的内部结构体型变量。 * @return 线程为有效则返回true,否则返回false。 */ static bool IsAlive(const ThreadStruct & ts) { if(NULL == ts.hThread) { return false; } DWORD dwRes = WaitForSingleObject(ts.hThread, 0); if(WAIT_TIMEOUT == dwRes) { return true; } else if(WAIT_OBJECT_0 == dwRes || WAIT_FAILED == dwRes) { return false; } else { return false; } } /** * 获取线程ID。该ID在进程域内唯一。 * @param ts线程实现类的内部结构体型变量。 */ static int GetThreadId(const ThreadStruct & ts) { return (int) ts.dwThreadId; } /** * 导致调用线程被挂起若干时间。 * @param iMs 线程挂起时间,毫秒单位。 */ static void Sleep(int iMs) { return ::Sleep(iMs); } private: // 禁止构造函数。 ThreadImpl(); // 禁止拷贝构造函数。 ThreadImpl(const ThreadImpl &) throw(); // 禁止赋值操作符。 void operator=(const ThreadImpl &); }; // class ThreadImpl #endif // #ifndef ThreadImpl_WIN32_H //----EOF---- 对上面的代码稍微做一点说明。 Linux版本的CreateThread()中,创建完线程后立即调用pthread_detach(),以通知POSIX线程库可以在该线程结束时立即释放线程资源。注意到我们不使用pthread_join()来等待线程结束。POSIX的现程模型使用pthread_join()来实现简单的线程调度,在笔者看来用起来很不方便,因为pthread_join()只能是无限等待,不提供超时机制,大大降低了程序设计的灵活性。 为了实现带超时的线程等待函数,如我们的WaitForThreadEnd(),我们不得不把等待时间分解成更小的时间片轮询。这样做损失了一点CPU资源,并稍微影响了超时判断的时间精度。 Windows版本的TerminateThread()中,我们将线程的结束代码(Exit code)一律指定为-1。我们的上层线程类不使用这个结束代码。 Linux版本和Windows版本的线程实现类都不提供线程优先级的相关功能。 |
|
来自: just_person > 《多线程类库的设计与实现》