在C++中封装Win32线程






4.63/5 (28投票s)
2001年7月27日
4分钟阅读

256443

4827
本文介绍了一个封装线程的类,让用户可以专注于项目细节。
目的
将 Win32 线程封装在 C++ 类中,易于继承和重用。隐藏线程的细节,以便用户可以专注于项目细节。
动机
C++ 等面向对象语言的优势在于能够封装对象的表示和实现,从而使编程更侧重于更高的抽象级别。我们说我们是在接口级别而不是函数级别进行编程。然而,大多数操作系统并不是为 C++ 设计的,它们通常使用非面向对象的方法来实现。这就是为什么有时封装某些平台相关的资源(例如线程)会很棘手。我的方法涵盖了 Win32 线程。
Win32 线程
要在同一进程中创建另一个线程,在 Win32 中需要使用几个处理线程的 API 函数。然而,它们是 C API 而不是 C++ API。我们可以很容易地注意到 C 语言的惯用法,如回调函数、`void*` 与其他类型之间的转换等等。让我们看看创建 Win32 线程的 API 函数 `CreateThread` 是如何工作的。下面是它的原型。
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
lpStartAddress 是一个指向将在新线程中运行的回调函数的指针,而 lpParameter 是一个传递给新线程的 `void*` 类型的参数。然而,传递回调函数的指针不符合 OOP 的精神,并且如果我们想将线程封装在类中,这会成为一个严重的障碍。传递给 `CreateThread` 的回调函数需要具有以下原型:
DWORD WINAPI ThreadProc( LPVOID lpParameter );
我们将注意到这个原型会阻止我们将此回调函数作为类的成员,因为它的成员函数会传递一个隐藏的参数:`this`。那么该怎么办呢?我们就此失败了吗?还没有。我们不能使用成员函数,而且我们已经看到了原因,但是类有静态方法,它们对所有对象只有一个实例。它们与类而不是对象相关联。这就是为什么它们不将 `this` 作为参数传递。所以静态函数成为回调函数的有趣选择。然而,有一个小问题,如果我们的线程在一个静态方法中,那么无论我们有多少个该类的对象,都只有一个线程,因为一个静态方法每个类只有一个实例。这并不是我们想要的。我们希望我们的工作线程成为一个成员方法,易于被子类重写,并且所有这些解决方法对客户端都是透明的。我们可以做到吗?可以。如果你看 `ThreadProc`,你会注意到它可以传递一个 `void*` 参数。没有什么可以阻止我们将 `(void*)this` 传递给它,而在 `ThreadProc` 中,我们只需调用我们的工作方法,因为我们现在有了这个指针。这样做的代码将如下所示:
//here we create the thread HANDLE CThread::CreateThread () { return ::CreateThread ((NULL, 0, (unsigned long (__stdcall *)(void *))this->runProcess, (void *)this, 0, NULL); } //static method int CThread::runProcess (void* pThis) { return ((CThread*)(pThis))->Process(); } //our working method, virtual, overridable int CThread::Process () { //will work in another thread }
到目前为止一切顺利。我们设法提供了一个线程机制的封装,因此用户只需实现自己的 `Process`,然后调用 `CreateThread` 成员函数。提供重用性甚至更容易,因为用户可以简单地继承我们上面定义的类 `CThread`,然后实现 `Process`,接着调用 `CreateThread`,这样就得到了一个简单的线程。然而,这里有一个小问题需要注意:假设我们有一个 `CThread` 的子类,名为 `CMyThread`。在 `CreateThread` 中,我们将 `this`(它是 `CMyThread*` 类型)转换为 `void*` 并将其传递给 `runProcess`,我们在其中将它转换回 `CThread`。C++ 标准规定,如果你将类型 `X*` 转换为 `void*`,那么只有在转换为相同的类型 `X*` 时才允许。其他转换会导致未定义的行为。这仅仅意味着我们做错了。我们该如何解决?嗯,可以通过一个小小的解决方法。
struct workAround { CThread* this_thread; }; //we pass a workAround struct instead of this HANDLE CThread::CreateThread () { workAround* wA = new workAround; wa->this_thread = this; return ::CreateThread ((NULL, 0, (unsigned long (__stdcall *)(void *))this->runProcess, (void *)wa, 0, NULL); } //static method int CThread::runProcess (void* pThis) { workAround* wA = (workAround*)pThis; //this will call the appropriate method, as Process is a virtual method CThread* thread = wA->this_thread; delete wA; return thread->Process(); }
这次我们是对的,因为我们正在与同一类型(`struct workAround`)进行转换。最后,为了遵循 C++ 的精神,我们将使用 C++ 转换,而不是 C 转换。例如,不是 `(void*)wA`,而是 `static_cast
结论
由于如今大多数操作系统都不是面向对象的,作为 C++ 开发者,我们在需要封装平台相关资源时通常必须寻找解决方法。我们必须采取不同的技巧来实现这一点,但一旦我们封装好它,它就会非常简单易用,并且可以大大减少精力来重用。Win32 中的线程就是这方面的一个很好的例子。你可以进一步研究源代码以获得更深入的见解。祝你编程愉快!