球胡麻差

按:球胡麻差,山西方言,乱七八糟之意。

C++对于托管代码的封装一向不是很尽善尽美,从最初的static成员函数到MFC的消息映射表,及至ATL的thunk机制。真可谓花样百出、层出不穷了。究其原因,这乃是C++的this指针惹的祸,这个“祸害”也就是Borland的VCL是用ObjectPASCAL编写的,而C++Buider只能提供VCL的动态链接之缘由了。
然而,我在不经意之间却获得了另一个封装的方法,完全脱离了static成员函数的一贯做法,并直接将非static成员函数指定为线程的托管代码——也许这听上去很神奇,其实不过尔尔,且听李马慢慢道来。

首先我将线程对象封装成一个纯虚基类ThreadObject,如下:

classThreadObject
{
public:
virtualvoidCreate()=0;
voidWait()
{
WaitForSingleObject(m_hThread,INFINITE);
CloseHandle(m_hThread);
}
protected:
virtualDWORDWINAPIDoWork(void)
{
for(inti=0;i<10;i++)
{
Sleep(rand()%1000);
printf("Thread%08Xisrunning.\n",m_dwThreadID);
}
return0;
}
DWORDm_dwThreadID;
HANDLEm_hThread;
};

这个类简单地封装了线程对象的数据成员及工作函数,下面我将基于这个类使用C++的继承来实现两种不同的托管封装。
首先是通常使用的方法。这种方法使用了一个static成员函数作为线程的托管代码,在创建线程的时候将类的this指针传入作为线程参数,代码大致如下:

classMyThread1:publicThreadObject
{
public:
voidCreate()
{
m_hThread=CreateThread(NULL,0,MyThread1::m_ThreadProc,this,0,&m_dwThreadID);
}
protected:
staticDWORDWINAPIm_ThreadProc(LPVOIDlpParam)
{
MyThread1*pThis=(MyThread1*)lpParam;
returnpThis->DoWork();
}
};

下面我来解释一下使用static成员函数的原因,也就是开头所说的“this指针惹的祸”。CreateThread所需要的线程入口函数是一个这样规格的函数:

DWORDWINAPIThreadProc(LPVOIDlpParameter);

如果使用了非static成员函数(诸位可以将m_ThreadProc前面的static去掉重新编译试试),那么编译器会给出类似这样的出错提示:

errorC2664:'CreateThread':cannotconvertparameter3from'unsignedlong(void*)'to'unsignedlong(__stdcall*)(void*)'

这是为什么呢?其实,C++的非static成员函数在编译器的处理下,会在参数中加入一个隐含的this指针,成为类似这个样子:

DWORDWINAPIMyThread1_m_ThreadProc(constMyThread1*this,LPVOIDlpParam);

这当然不符合我们预期的调用约定。于是,严格的C++编译器就会在发生类似这样的类型转换的时候予以坚决制止。不过,当我回头望到基类中的这个函数的时候,突然眼前一亮:

DWORDWINAPIThreadObject::DoWork(void);

我想,这个函数经过this指针处理后,应该会变成类似这个样子:

DWORDWINAPIThreadObject_DoWork(constThreadObject*this);

一个指针参数,这倒是非常符合线程函数的规格了。于是,我写出了如下的代码:

LPVOIDp=(LPVOID)DoWork;
LPTHREAD_START_ROUTINEpFunc=(LPTHREAD_START_ROUTINE)p;
m_hThread=CreateThread(NULL,0,pFunc,this,0,&m_dwThreadID);

结果令人失望,因为编译器根本不允许将DoWork转换成LPVOID。百无聊赖之中,我随手写下了这样的代码:

LPTHREAD_START_ROUTINEpFunc=(LPTHREAD_START_ROUTINE)0x12345;

这段代码竟然能够编译成功(不过当然不能执行,否则程序必然当掉),于是,我将目光移到了虚函数表上。我可以通过this指针获取虚函数表指针vptr的值,然后经由这个指针获得虚函数表,那么这个表的第二个栏位自然就是DoWork的地址了!于是我重新振作起来,完成了我的线程类:

classMyThread2:publicThreadObject
{
public:
voidCreate()
{
//首先获得vtable的指针vptr
DWORD**pVptr=(DWORD**)this;
//经由虚函数表获得DoWork的地址进行调用
LPTHREAD_START_ROUTINEpFunc=(LPTHREAD_START_ROUTINE)(*pVptr)[1];//(*pVptr)[0]为Create
m_hThread=CreateThread(NULL,0,pFunc,this,0,&m_dwThreadID);
}
};

那么,现在可以对比测试一下了:

MyThread1t1;
MyThread2t2;
t1.Create();
t2.Create();
t1.Wait();
t2.Wait();

这就是我花了半个下午的时间封装出来的代码。走笔至此,我突然问自己:这半个下午我到底做了什么?就是这么一段非常有暴力倾向甚至有些变态的代码吗?呃……的确是这样,因此我还是建议你使用MyThread1的托管封装做法。至于我的做法,我仍然希望它能多少带给你一些启发或警示,使得它还不至于完全没用。
真是 球胡麻差

你可能感兴趣的:(C++,c,C#,mfc,Borland)