直到现在我在工作中接触到的多线程任务都是“无需用户输入”的,也就是说很少涉及到窗口消息等,一般就是计算、和I/O接口的通讯,这些都属于MFC定义的工作者线程(worker thread)范畴。工作者线程的开启(包括退出)是多线程编程入门级技术。虽然启动一个工作者线程非常简单,有时候一个CreateThread函数即可,但稍微深究便发现要游刃有余的运用它必须要有较全面的理解。
1. 关于::CreateThread和CWinThread::CreateThread
初学者(如我)在看MSDN以及既成代码的时候,遇到CreateThread就只知道是创建线程,而不知其名虽同,姓却不同,完全不是一家人。::CreateThread是Windows API函数,程序员直接调用该函数生成的只是类似MFC概念中的工作者线程;CWinThread::CreateThread是MFC类库函数,可以生成真正MFC概念中的工作者线程或用户接口线程。程序员直接调用::CreateThread的位置通常不在CWinThread或其派生类中,可去掉二元作用域操作符“::”,直接写作“CreateThread”,这样就更容易让初学者迷惑而不区分这两种完全不同的函数。
区分这两种函数的关键是理解CWinThread类的运用,为什么要把线程抽象成一个类?
回想线程的概念,它是进程的执行路径,进程的所有代码和数据空间被进程内所有的线程共享,如全局变量。于是我们知道,线程是一种有方法(函数代码)有属性(数据空间)的事物,可被抽象;另外,有些数据是不想在线程以外的位置被修改的,虽然可以用代码对全局变量进行保持和维护,但增加了工作量且易出错。于是,MFC便将线程抽象出来,定义了CWinThread类。CWinThread对象不但管理线程的局部变量,还提供了对MFC“消息泵”的支持。使用CWinThread及其派生类,使得代码和MFC都具有完全线程安全的特性,该特性要求任何使用到MFC的线程必须是CWinThread或其派生类。
2. 创建非面向对象的简单工作者线程
非面向对象的简单工作者线程,是指控制函数不具有面向对象性,可被所有线程调用。一般说来有三种方法:::CreateThread、AfxBeginThread、CWinThread::CreateThread。说到这里可能又会引起一些迷糊,不是说MFC引入了面向对象特性么,为什么用MFC函数(后两个)创建的却是非面向对象的工作者线程呢?这涉及到回调(callback)函数的概念。线程函数由程序员编写、由操作系统根据函数指针进行调用,这种类型的函数有别于直接用函数名进行调用的函数,被称作回调函数。回调函数不能有隐含的this指针,故只能是全局函数或类的静态成员函数。因此简单工作者线程只能是非面向对象或具有非常有限的面向对象特性。
创建简单工作者线程需要定义一个型如:
UINT MyControllingFunction ( LPVOID pParam );的控制函数。参数为一个32-bit无符号整形,可以是一个具体数据、一个指针,也可以为空;返回“0”代表正常,其他值代表错误类型;&MyControllingFunction作为回调函数地址。
MSDN文档中对用::CreateThread、AfxBeginThread创建线程的方法有很详细的介绍了,只要把回调函数地址和控制函数的参数作为实参填入适当位置便可。文档中只说了AfxBeginThread最终调用CWinThread::CreateThread启动线程,而CWinThread::CreateThread的参数表中也没有可填回调函数地址的地方,看似无法用其创建工作者线程,其实恰恰相反,三种方法中最为灵活的便是CWinThread::CreateThread。它能创建简单工作者线程,只要借助两个文档中没有提到的CWinThread类公共成员变量:AFX_THREADPROC m_pfnThreadProc(回调函数地址)、LPVOID m_pThreadParams(控制函数参数),启动线程前显式将这两个参数赋上适当的值。
3. 创建具有面向对象特性的工作者线程
虽然有回调函数的要求,我们还是能够创建具有面向对象特性的工作者线程。具有面向对象特性的工作者线程有如下特点:1)没有消息循环;2)定义一个函数,可被不同线程调用,且仅影响线程内的局部变量。有两种方法:
方法一:定义具有面向对象特性的控制函数,型如:
UINT MyThread::MyControllingFunction();再定义类的静态成员函数作为回调函数,型如:
static UINT MyThread::MyCallbackFunction ( LPVOID pParam );将this作为参数传递给该静态成员函数,并在其中调用类的成员函数:this-> MyControllingFunction()。该方法的具体实现可用AfxBeginThread或CWinThread::CreateThread。它有个局限,实际控制函数的参数只能为空。
方法二:不定义回调函数(控制函数地址为NULL),直接重载CWinThread::InitInstance(经常)或CWinThread::Run(几乎不)作为控制函数。观察MFC与操作系统接口的线程入口函数_AfxThreadEntry便知其原因:
UINT APIENTRY _AfxThreadEntry(void* pParam)CWinThread::Run()函数支持“消息泵”,是用户接口线程的执行地址,若将其重载为工作者线程的控制函数将不再具有这个特性。该方法的具体实现亦可用AfxBeginThread或CWinThread::CreateThread,还可通过变量m_pThreadParams设置控制函数的参数。另外,由该方法可知MFC的工作者线程控制函数不一定是回调函数,MFC已经为操作系统提供了一个回调函数_AfxThreadEntry。
{
…
// first -- check for simple worker thread
DWORD nResult = 0;
if (pThread->m_pfnThreadProc != NULL)
//该方法中将此参数设为NULL
{
nResult = (*pThread->m_pfnThreadProc)(pThread->m_pThreadParams);
ASSERT_VALID(pThread);
}
// else -- check for thread with message loop
else if (!pThread->InitInstance())
//重载InitInstantce作为控制函数
//若重载InitInstance需将其返回值设为FALSE
{
ASSERT_VALID(pThread);
nResult = pThread->ExitInstance();
}
else
{
// will stop after PostQuitMessage called
ASSERT_VALID(pThread);
nResult = pThread->Run(); //或者重载Run作为控制函数
}
…
}
本文介绍了5种工作者线程的创建方法,编程时可根据线程中是否用到MFC、是否需要面向对象特性等进行选择。
没有评论:
发表评论