MFC 应用程序中的所有线程都由 CWinThread 对象表示。 在大多数情况下,你甚至不必显式创建这些对象;而是调用框架帮助程序函数 AfxBeginThread,该函数会为你创建 CWinThread 对象。
MFC 区分两种类型的线程:用户界面线程和工作线程。 用户界面线程通常用于处理用户输入并响应用户生成的事件和消息。 工作线程通常用于完成无需用户输入的任务。 MFC 通过为用户界面中的事件提供消息泵,专门处理用户界面线程。 CWinApp 是用户界面线程对象的一个示例,因为它派生自 CWinThread 并处理用户生成的事件和消息。
应用程序由一个或多个进程组成。 最简单的术语 进程 是一个正在执行的程序。 一个或多个线程在进程的上下文中运行。 线程 是作系统向其分配处理器时间的基本单元。 线程可以执行进程代码的任何部分,包括当前由另一个线程执行的部件。
线程池 是工作线程的集合,可有效地代表应用程序执行异步回调。 线程池主要用于减少应用程序线程数,并提供工作线程的管理。
创建 MFC 用户界面线程
用户界面线程通常用于处理用户输入和响应用户事件,独立于执行应用程序其他部分的线程。 已为你创建并启动主应用程序线程(在 CWinApp 派生类中提供)。
创建用户界面线程时必须做的第一件事是从 CWinThread 派生类。 必须使用 DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 宏声明和实现此类。 此类必须重写某些函数,也可以重写其他函数。
创建用户界面线程时要重写的函数
| 功能 | 目的 |
|---|---|
| ExitInstance | 线程终止时执行清理。 通常会被重写。 |
| InitInstance | 执行线程实例初始化。 必须被重写。 |
| OnIdle | 执行特定于线程的空闲时间处理。 通常不会被重写。 |
| PreTranslateMessage | 在将消息调度到 TranslateMessage 和 DispatchMessage 之前对其进行筛选。 通常不会被重写。 |
| ProcessWndProcException | 截获由线程的消息和命令处理程序引发的未经处理的异常。 通常不会被重写。 |
| Run | 控制线程的函数。 包含消息泵。 很少被重写。 |
MFC 通过参数重载提供两个版本的 AfxBeginThread:一个只能创建辅助线程,另一个既可创建用户界面线程也可创建辅助线程。 若要启动用户界面线程,请调用 AfxBeginThread 的第二个重载。
AfxBeginThread 为你执行大部分工作。 它会创建类的新对象,使用你提供的信息初始化它,并调用 CWinThread::CreateThread 以开始执行线程。 在整个过程中都会进行检查,以确保在创建的任何部分失败时正确解除分配所有对象。
cpp
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL);
范例










在 MFC 中创建工作线程
创建工作线程相对简单。 只需完成两步就能使线程运行:实现控制函数并启动线程
启动线程
有两个重载版本的 AfxBeginThread:一个只能创建工作线程,一个既可创建用户界面线程也可创建工作线程。 若要使用第一个重载开始执行工作线程,请调用 AfxBeginThread。
AfxBeginThread 为你创建和初始化 CWinThread 对象,启动对象,并返回其地址以便你稍后可以引用它。 在整个过程中都会进行检查,以确保在创建的任何部分失败时正确解除分配所有对象。
实现控制函数
控制函数定义线程。 输入此函数时,线程启动,退出时线程终止。 此函数应具有以下原型:
cpp
UINT MyControllingFunction( LPVOID pParam );
该参数是单个值。 函数在此参数中接收的值是创建线程对象时传递给构造函数的值。 控制函数可以采用它选择的任何方式解释此值。 可将它视为标量值或指向包含多个参数的结构的指针,也可以忽略它。 如果参数引用结构,则结构不仅可用于将数据从调用方传递到线程,还可用于将数据从线程传回调用方。
函数终止时,应返回指示终止原因的 UINT 值。 通常,此退出代码为 0 表示成功,其他值指示不同类型的错误。 这完全依赖于实现。
控制函数示例
cpp
//工作线程定义
UINT MyThreadProc( LPVOID pParam )
{
CMyObject* pObject = (CMyObject*)pParam;
if (pObject == NULL ||
!pObject->IsKindOf(RUNTIME_CLASS(CMyObject)))
return 1; // if pObject is not valid
//用户代码
return 0; // thread completed successfully
}
// 程序内
.
pNewObject = new CMyObject;//传递参数
AfxBeginThread(MyThreadProc, pNewObject);//启动工作线程
.
范例
cpp
int i = 0, j = 0;
UINT MyThread1(LPVOID aParam)
{
for (; i < 1000; i++)
{
std::cout << "MyThread1:" << i << endl;
}
return 0;
}
UINT MyThread2(LPVOID aParam)
{
for (; j < 1000; j++)
{
std::cout << "MyThread2:" << j << endl;
}
return 0;
}
int main()
{
CWinThread* pThread1 = AfxBeginThread(MyThread1,0, THREAD_PRIORITY_NORMAL,0);
CWinThread* pThread2 = AfxBeginThread(MyThread2, 0, THREAD_PRIORITY_HIGHEST, 0);
// TODO: 在此处为应用程序的行为编写代码。
if (pThread1 && pThread2) {
// 等待线程结束
WaitForSingleObject(pThread1->m_hThread, INFINITE);
WaitForSingleObject(pThread2->m_hThread, INFINITE);
}
//Sleep(1000);
return 0;
}

在 MFC 中终止线程
普通线程终止
对于工作线程,普通线程终止很简单:退出控制函数并返回一个表示终止原因的值。 可以使用 AfxEndThread 函数或 return 语句。
对于用户界面线程,过程同样简单:从用户界面线程内部调用 Windows SDK 中的 PostQuitMessage。
线程提前终止
过早终止线程几乎一样简单:从线程内部调用 AfxEndThread。AfxEndThread 必须从要终止的线程中调用。 如果要从另一个线程终止线程,则必须在两个线程之间设置通信方法。
检索线程的退出代码
若要获取工作线程或用户界面线程的退出代码,请调用 GetExitCodeThread 函数。
如果线程仍处于活动状态,GetExitCodeThread 会将 STILL_ACTIVE 放置在提供的 DWORD 地址中;否则,退出代码将放置在此地址中。
检索 CWinThread 对象的退出代码需要执行额外的步骤。 默认情况下,当 CWinThread 线程终止时,线程对象将删除。 这意味着将无法访问 m_hThread 数据成员,因为 CWinThread 对象不再存在。 为避免这种情况,请执行下列操作之一:
-
将
m_bAutoDelete数据成员设置为 FALSE。 这样,CWinThread对象就可以在线程终止后继续存在。 然后,可以在线程终止后访问m_hThread数据成员。 但如果使用此方法,则需负责销毁CWinThread对象,因为框架不会自动删除该对象。 这是首选方法。 -
单独存储线程的句柄。 创建线程后,将其
m_hThread数据成员(使用::DuplicateHandle)复制到另一个变量并通过该变量访问它。 这样当终止发生时,对象会自动删除,而你仍然可以找出线程终止的原因。 请注意,复制句柄之前,线程不会终止。 最安全的方法是将 CREATE_SUSPENDED 传递给 AfxBeginThread,存储句柄,然后通过调用 ResumeThread 继续线程。
多线程处理
从多个线程访问对象
MFC 对象本身不是线程安全的。 除非你使用 MFC 同步类和/或适当的 Win32 同步对象(如临界区),否则两个单独的线程无法操作同一对象。 有关临界区和其他相关对象的详细信息,请参阅 Windows SDK 中的同步。
类库在内部使用临界区来保护全局数据结构,例如调试内存分配所使用的临界区。
从非 MFC 线程访问 MFC 对象
如果你有一个多线程应用程序,它创建线程的方式不是使用 CWinThread 对象,则你无法从该线程访问其他 MFC 对象。 换句话说,若要从辅助线程访问任何 MFC 对象,则必须使用多线程:创建用户界面线程或多线程:创建工作线程中所述的方法之一创建该线程。 这些方法是唯一允许类库初始化那些处理多线程应用程序所需的内部变量的方法。
Windows 句柄映射
一般说来,线程只能访问其创建的 MFC 对象。 这是因为临时的和永久的 Windows 句柄映射保存在线程本地存储中,目的是阻止从多个线程同时进行的访问。
例如,工作线程无法在执行计算后调用文档的 UpdateAllViews 成员函数来修改包含有关新数据的视图的窗口。 这完全不起作用,因为从 CWnd 对象到 HWND 的映射是主线程的本地映射。 这意味着一个线程可能有从 Windows 句柄到 C++ 对象的映射,但另一个线程可能会将该句柄映射到不同的 C++ 对象。 在一个线程中所做的更改不会反映在另一个线程中。
有几种方法可以避免此问题。
第一种是将单个句柄(例如 HWND)而不是 C++ 对象传递给工作线程。 然后,工作线程通过调用相应的 FromHandle 成员函数将这些对象添加到其临时映射中。 还可以通过调用 Attach 将对象添加到线程的永久映射中,但只能在确保对象存在的时间超过线程存在的时间时执行此操作。
另一种方法是创建新的用户定义消息(这些消息对应于将由工作线程执行的不同任务),并使用 ::PostMessage 将这些消息发布到应用程序的主窗口。 这种通信方法类似于两个不同的应用程序进行会话,但两个线程都在相同的地址空间中执行。