MFC多线程
1 .MFC 对 多 线 程 的 支 持
MFC 类 库 提 供 了 多 线 程 编 程 支 持, 对 于 用 户编 程 实 现 来 说 更 加 方 便。 非 常 重 要 的 一 点 就 是, 在 多 窗 口 线 程 情 况 下,MFC 直 接 提 供 了 用 户 接 口线 程 的 设 计。
MFC 区 分 两 种 类 型 的 线 程: 辅 助 线 程(Worker Thread) 和 用 户 界 面 线 程(UserInterface Thread)。 辅 助 线 程 没 有 消 息 机 制, 通 常 用 来 执 行 后台 计 算 和 维 护 任 务。 MFC 为 用 户 界 面 线 程 提 供 消 息 机 制, 用 来 处 理 用 户 的 输 入, 响 应 用 户 产 生 的事 件 和 消 息。 但 对 于Win32 的API 来 说, 这 两 种 线 程 并 没 有 区 别, 它 只 需 要 线 程 的 启 动 地 址 以 便启 动 线 程 执 行 任 务。 用 户 界 面 线 程 的 一 个 典 型 应 用 就 是 类CWinApp, 大 家 对 类CwinApp 都 比 较 熟悉, 它 是CWinThread 类 的 派 生 类, 应 用 程 序 的 主 线 程 是 由 它 提 供, 并 由 它 负 责 处 理 用 户 产 生 的事 件 和 消 息。 类CwinThread 是 用 户 接 口 线 程 的 基 本 类。CWinThread 的 对 象 用 以 维 护 特 定 线 程 的局 部 数 据。 因 为 处 理 线 程 局 部 数 据 依 赖 于 类CWinThread, 所 以 所 有 使 用MFC 的 线 程 都 必 须 由MFC 来 创 建。 例 如, 由run-time 函 数_beginthreadex 创 建 的 线 程 就 不 能 使 用 任 何MFC API。
2 . 辅 助 线 程 和 用 户 界 面 线 程 的 创 建 和 终止
要 创 建 一 个 线 程, 需 要 调 用 函 数AfxBeginThread。该 函 数 通 过 参 数 重 载 具 有 两 种 版 本, 分 别 对 应 辅 助 线 程 和 用 户 界 面 线 程。 无 论 是 辅 助 线 程 还 是用 户 界 面 线 程, 都 需 要 指 定 额 外 的 参 数 以 修 改 优 先 级, 堆 栈 大 小, 创 建 标 志 和 安 全 特 性 等。 函 数AfxBeginThread 返 回 指 向CWinThread 类 对 象 的 指 针。
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc,//控制函数
LPVOID pParam,// 传送给控制函数的参数
int nPriority = THREAD_PRIORITY_NORMAL,// 线程的优先级
UINT nStackSize = 0,// 新线程使用的栈的以字节为单位的大小
DWORD dwCreateFlags = 0,// CREATE_SUSPENDED或0
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL//安全特性
);
创 建 助 手 线 程
相 对 简 单。 只 需 要 两 步: 实 现 控制 函 数 和 启 动 线 程。 它 并 不 必 须 从CWinThread 派 生 一 个 类。 简 要 说 明 如 下:
1. 实 现 控 制 函 数。 控 制 函 数 定 义 该 线 程。 当进 入 该 函 数, 线 程 启 动; 退 出 时, 线 程 终 止。 该 控 制 函 数 声 明 如 下:
UINT MyControllingFunction( LPVOID pParam );
该 参 数 是 一 个 单 精 度32 位 值。 该 参 数 接 收 的值 将 在 线 程 对 象 创 建 时 传 递 给 构 造 函 数。 控 制 函 数 将 用 某 种 方 式 解 释 该 值。 可 以 是 数 量 值, 或是 指 向 包 括 多 个 参 数 的 结 构 的 指 针, 甚 至 可 以 被 忽 略。 如 果 该 参 数 是 指 结 构, 则 不 仅 可 以 将 数据 从 调 用 函 数 传 给 线 程, 也 可 以 从 线 程 回 传 给 调 用 函 数。 如 果 使 用 这 样 的 结 构 回 传 数 据, 当 结果 准 备 好 的 时 候, 线 程 要 通 知 调 用 函 数。 当 函 数 结 束 时, 应 返 回 一 个UINT 类 型 的 值, 指 明 结 束的 原 因。 通 常, 返 回0 表 明 成 功, 其 它 值 分 别 代 表 不 同 的 错 误。
2. 启 动 线 程。 由 函 数AfxBeginThread 创 建 并初 始 化 一 个CWinThread 类 的 对 象, 启 动 并 返 回 该 线 程 的 地 址。 则 线 程 进 入 运 行 状 态。
3. 举 例 说 明。 下 面 用 简 单 的 代 码 说 明 怎 样 定义 一 个 控 制 函 数 以 及 如 何 在 程 序 的 其 它 部 分 使 用。
UINT MyThreadProc( LPVOID pParam )
{
CMyObject* pObject = (CMyObject*)pParam;
if (pObject == NULL ||
!pObject- >IsKindOf(RUNTIME_CLASS(CMyObject)))
return -1; //非法参数
……//具体实现内容
return 0; //线程成功结束
}
//在程序中调用线程的函数
……
pNewObject = new CMyObject;
AfxBeginThread(MyThreadProc, pNewObject);
……
创建用户界面线程
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,// 从CWinThread继承的对象的RUNTIME_CLASS
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
创建用户界面线程有两种方法。
第 一 种 方 法,
首先, 从CWinTread 类 派 生 一 个 子类(注 意 必 须 要 用 宏DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 对 该 类 进 行 声 明 和 实 现);
然 后, 调 用 函 数AfxBeginThread 创 建CWinThread 派 生 类 的 对 象 进 行 初 始 化 启 动 线 程 运 行。
第 二 种 方 法,
首先, 即 先 通 过 构 造 函 数 创 建 类CWinThread 的 一个 对 象,
然后, 由 程 序 员 调 用 函 数::CreateThread 来 启 动 线 程。
CWinThread::CreateThread
BOOL CreateThread(DWORD dwCreateFlags = 0,
UINT nStackSize = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL
);
通 常 类CWinThread 的 对 象 在 该 线程 的 生 存 期 结 束 时 将 自 动 终 止, 如 果 程 序 员 希 望 自 己 来 控 制, 则 需 要 将m_bAutoDelete 设 为FALSE。这 样 在 线 程 终 止 之 后 类CWinThread 对 象 仍 然 存 在, 只 是 在 这 种 情 况 下 需 要 手 动 删 除CWinThread 对 象。
通 常 线 程 函 数 结 束 之 后, 线 程 将 自 行 终 止。 类CwinThread 将 为 我 们 完 成 结 束 线 程 的 工 作。 如 果 在 线 程 的 执 行 过 程 中 程 序 员 希 望 强 行 终 止 线 程 的 话, 则 需要 在 线 程 内 部 调 用AfxEndThread(nExitCode)。 其 参 数 为 线 程 结 束 码。 这 样 将 终 止 线 程 的 运 行,并 释 放 线 程 所 占 用 的 资 源。
如 果 从 另 一 个 线 程 来 终 止 该 线 程, 则 必 须 在 两 个 线 程 之 间 设 置 通 信方 法。 如 果 从 线 程 外 部 来 终 止 线 程 的 话, 还 可 以 使 用Win32 函 数(CWinThread 类 不 提 供 该 成 员 函数):BOOL TerminateThread(HANDLE hThread,DWORD dwExitcode)。 但 在 实 际 程 序 设 计 中 对 该函 数 的 使 用 一 定 要 谨 慎, 因 为 一 旦 该 命 令 发 出, 将 立 即 终 止 该 线 程, 并 不 释 放 线 程 所 占 用 的 资源, 这 样 可 能 会 引 起 系 统 不 稳 定。
如 果 所 终 止 的 线 程 是 进 程 内 的 最 后 一 个 线 程,则 在 该 线 程 终 止 之 后 进 程 也 相 应 终 止。
3 进 程 和 线 程 的 优 先 级 问 题
在Windows95 和WindowsNT 操 作 系 统 当 中, 任务 是 有 优 先 级 的, 共 有32 级, 从0 到31, 系 统 按 照 不 同 的 优 先 级 调 度 线 程 的 运 行。
1) 0-15 级 是 普 通 优 先 级, 线 程 的 优 先 级 可以 动 态 变 化。 高 优 先 级 线 程 优 先 运 行, 只 有 高 优 先 级 线 程 不 运 行 时, 才 调 度 低 优 先 级 线 程 运 行。优 先 级 相 同 的 线 程 按 照 时 间 片 轮 流 运 行。
2) 16-30 级 是 实 时 优 先 级, 实 时 优 先 级 与 普 通 优 先级 的 最 大 区 别 在 于 相 同 优 先 级 进 程 的 运 行 不 按 照 时 间 片 轮 转, 而 是 先 运 行 的 线 程 就 先 控 制CPU,如 果 它 不 主 动 放 弃 控 制, 同 级 或 低 优 先 级 的 线 程 就 无 法 运 行。
一 个 线 程 的 优 先 级 首 先 属 于 一 个 类, 然 后 是其 在 该 类 中 的 相 对 位 置。 线 程 优 先 级 的 计 算 可 以 如 下 式 表 示:
线 程 优 先 级= 进 程 类 基 本 优 先 级+ 线 程 相 对 优先 级
进 程 类 的 基 本 优 先 级:
IDLE_PROCESS_CLASS
NORMAL_PROCESS_CLASS
HIGH_PROCESS_CLASS
REAL_TIME_PROCESS_CLASS
线程的相对优先级:
THREAD_PRIORITY_IDLE
(最低优先级,仅在系统空闲时执行)
THREAD_PRIORITY_LOWEST
THREAD_PRIORITY_BELOW_NORMAL
THREAD_PRIORITY_NORMAL (缺省)
THREAD_PRIORITY_ABOVE_NORMAL
THREAD_PRIORITY_HIGHEST
THREAD_PRIORITY_CRITICAL
(非常高的优先级)
4 线 程 同 步 问 题
编 写 多 线 程 应 用 程 序 的 最 重 要 的 问 题 就 是 线程 之 间 的 资 源 同 步 访 问。 因 为 多 个 线 程 在 共 享 资 源 时 如 果 发 生 访 问 冲 突 通 常 会 产 生 不 正 确 的结 果。 例 如, 一 个 线 程 正 在 更 新 一 个 结 构 的 内 容 的 同 时 另 一 个 线 程 正 试 图 读 取 同 一 个 结 构。 结果, 我 们 将 无 法 得 知 所 读 取 的 数 据 是 什 么 状 态: 旧 数 据, 新 数 据, 还 是 二 者 的 混 合 ?
MFC 提 供 了 一 组 同 步 和 同 步 访 问 类 来 解 决 这个 问 题, 包 括:
同 步 对 象:CSyncObject, CSemaphore, CMutex, CcriticalSection 和CEvent ;
同 步 访 问 对 象:CMultiLock 和 CSingleLock 。
同 步 类 用 于 当 访 问 资 源 时 保 证 资 源 的 整 体 性。其 中
CsyncObject 是 其 它 四 个 同 步 类 的 基 类, 不 直 接 使 用。
信 号 同 步 类CSemaphore 通 常 用 于 当一 个 应 用 程 序 中 同 时 有 多 个 线 程 访 问 一 个 资 源( 例 如, 应 用 程 序 允 许 对 同 一 个Document 有 多 个View)的 情 况;
事 件 同 步 类CEvent 通 常 用 于 在 应 用 程 序 访 问 资 源 之 前 应 用 程 序 必 须 等 待( 比 如, 在 数据 写 进 一 个 文 件 之 前 数 据 必 须 从 通 信 端 口 得 到) 的 情 况;
而 对 于 互 斥 同 步 类CMutex 和 临 界 区 同步 类CcriticalSection 都 是 用 于 保 证 一 个 资 源 一 次 只 能 有 一 个 线 程 访 问, 二 者 的 不 同 之 处 在于 前 者 允 许 有 多 个 应 用 程 序 使 用 该 资 源( 例 如, 该 资 源 在 一 个DLL 当 中) 而 后 者 则 不 允 许 对 同 一个 资 源 的 访 问 超 出 进 程 的 范 畴, 而 且 使 用 临 界 区 的 方 式 效 率 比 较 高。
同 步 访 问 类 用 于 获 得 对 这 些 控 制 资 源 的 访 问。CMultiLock 和 CSingleLock 的 区 别 仅 在 于 是 需 要 控 制 访 问 多 个 还 是 单 个 资 源 对 象。
5 同 步 类 的 使 用 方 法
解 决 同 步 问 题 的 一 个 简 单 的 方 法 就 是 将 同 步类 融 入 共 享 类 当 中, 通 常 我 们 把 这 样 的 共 享 类 称 为 线 程 安 全 类。
下 面 举 例 来 说 明 这 些 同 步 类 的使 用 方 法。
比 如, 一 个 用 以 维 护 一 个 帐 户 的 连 接 列 表 的 应 用 程 序。 该 应 用 程 序 允 许3 个 帐 户 在 不同 的 窗 口 中 检 测, 但 一 次 只 能 更 新 一 个 帐 户。 当 一 个 帐 户 更 新 之 后, 需 要 将 更 新 的 数 据 通 过 网络 传 给 一 个 数 据 文 档。
该 例 中 将 使 用3 种 同 步 类。 由 于 允 许 一 次 检 测3 个 帐 户, 使 用CSemaphore 来 限 制 对3 个 视 窗 对 象 的 访 问。 当 更 新 一 个 帐 目 时, 应 用 程 序 使 用CCriticalSection 来 保 证 一 次 只 有 一 个 帐 目 更 新。 在 更 新 成 功 之 后, 发CEvent 信 号, 该 信 号 释 放 一 个 等 待 接 收 信号 事 件 的 线 程。 该 线 程 将 新 数 据 传 给 数 据 文 档。
要 设 计 一 个 线 程 安 全 类, 首 先 根 据 具 体 情 况在 类 中 加 入 同 步 类 做 为 数 据 成 员。 在 例 子 当 中, 可 以 将 一 个CSemaphore 类 的 数 据 成 员 加 入 视 窗类 中, 一 个CCriticalSection 类 数 据 成 员 加 入 连 接 列 表 类, 而 一 个CEvent 数 据 成 员 加 入 数 据 存储 类 中。
然 后, 在 使 用 共 享 资 源 的 函 数 当 中, 将 同 步 类与 同 步 访 问 类 的 一 个 锁 对 象 联 系 起 来。 即, 在 访 问 控 制 资 源 的 成 员 函 数 中 应 该 创 建 一 个CSingleLock 或 CMultiLock 的 对 象 并 调 用 该 对 象 的Lock 函 数。 当 访 问 结 束 之 后, 调 用UnLock 函 数, 释 放 资 源。
用 这 种 方 式 来 设 计 线 程 安 全 类 比 较 容 易。 在保 证 线 程 安 全 的 同 时, 省 去 了 维 护 同 步 代 码 的 麻 烦, 这 也 正 是OOP 的 思 想。 但 是 使 用 线 程 安 全 类方 法 编 程 比 不 考 虑 线 程 安 全 要 复 杂, 尤 其 体 现 在 程 序 调 试 过 程 中。 而 且 线 程 安 全 编 程 还 会 损 失一 部 分 效 率, 比 如 在 单CPU 计 算 机 中 多 个 线 程 之 间 的 切 换 会 占 用 一 部 分 资 源。