多线程编程技术开发资料
目录
Win32 多线程的性能(1)... 1
Win32 多线程的性能(2)... 10
关于多线程的一些细节... 23
用VC++5.0 实 现 多 线 程 的 调 度 和 处 理... 25
一 多 任 务, 多 进 程 和 多 线 程... 25
二 基 于MFC 的 多 线 程 编 程... 26
三 编 程 实 例... 29
用VC++5实现多线程... 35
Windows95下多线程编程技术及其实现... 40
多线程编程应注意的问题... 44
多线程程序设计... 45
Visual C++ 5.0中的多线程编程技术... 50
关于线程... 62
采用多线程进行数据采集... 64
循环创建多线程时保证参数的有效性... 67
MFC中多线程的应用... 70
线程通信初探... 84
VC++多线程下内存操作的优化... 88
任务,过程,和线程... 95
使用临界段实现优化的进程间同步对象-原理和实现... 100
作者:Microsoft公司供稿 |
Ruediger R. Asche |
作者:Microsoft公司供稿 |
Ruediger R. Asche |
作者: coolnerd
线程的程序中,如果线程要向界面窗口报告状态,有两种操作方法,
一种是通过消息的方法,由于消息本身携带的消息量有时不购用,往往消息参数
只是一个指向某消息对象的指针,而消息对象往往需要在堆内存中new生成,
(因为往往线程不能等待消息处理完毕就继续执行,所以如果消息对象是栈对象
往往消息对象还未来及被处理,就又被线程修改.所以采用堆对象.)
界面接受到
消息对象后delete之.但是这时界面退出后,如果线程仍然生成新的消息对象,
则消息对象得不到释放,所以在这种情况下,界面接受到WM_CLOSE消息将要释放
之前,要等待线程完全退出之后再真正释放.
线程向界面报告状态的第二种方法是直接在线程的执行过程中同步地(等待,
直到完成称为同步)执行界面显示,这种机制下,要注意在界面显示是需要查看
界面窗口是否仍然存在(使用IsWindow(hWnd)函数实现).
这样做似乎已经完美,但是是不完善的,因为假如有多个view小窗口,如多个
CSplitterWnd,只在一个CSplitterWnd的WM_CLOSE消息的处理函数中进行防范,
其他的CSplitterWnd照常退出,仍然要出问题,所以要抓住根源:
用户使用菜单退出或点击frmae窗口的x按钮退出,接受到退出消息的首先是frameWnd
所以需要在frameWnd的WM_CLOSE函数中进行线程的释放.!
另外,往往线程在Document类的掌管之下,frame怎样访问document对象?
FrameWnd没有直接提供获取document的函数.Document,View,FrameWnd三者的
创建顺序是:doc->Frmae->View,在View::InitUpdate()函数的执行时刻,
可以执行以下代码:
CMainFrame*frm=(CMainFrame*)(AfxGetApp()->m_pMainWnd);
frm->pDoc=GetDocument();
另外:注意需要捕捉WM_CLOSE,而非WM_DESTROY消息,因为WM_CLOSE消息
先于后者.
另外,考查以下代码:
void CThreadList::UpdateThread(int id,CString client,CString msg)
{
EnterCriticalSection(&CThreadList::csUpDateThread);
{
int ItemCount=m_ListCtrl.GetItemCount() ; //ListCtrl
最多65535条记录
if(id>ItemCount)
{
for(int i=0;i
LV_ITEM lvi;
lvi.mask = LVIF_TEXT | LVIF_IMAGE
/* |LVIF_STATE */;
lvi.iItem = ItemCount+i;
lvi.iSubItem = 0;
m_ListCtrl.InsertItem(&lvi);
//m_ListCtrl.SetItemCount(id);
}
}
m_ListCtrl.SetItemText(id-1,0,ito10a(id));
m_ListCtrl.SetItemText(id-1,1,client);
m_ListCtrl.SetItemText(id-1,2,msg);
}
LeaveCriticalSection(&CThreadList::csUpDateThread);
}
这就是线程用来调用的界面函数,该界面CThreadList
是个ListCtrl类,该成员函数的参数中,id,client,msg是界面显示的内容
函数首先判断id是否超出现在已经存在的个数,如果超出则增加一到多条记录
这种动态调整记录个数的机制比较诱人,但是如果这个函数是在这样的情况下
被调用: frame窗口接受到了WM_CLOSE消息,在处理消息之前,首先消灭线程
而就在消灭线程的过程中,一个未来及消灭的线程调用了这个函数,该函数在
执行过程中需要执行GetItemCount()函数,跟踪GetItemCount函数,发现它是
依靠执行SendMessage获得ItemCount的(SendMessage函数是个不等到结果不
返回的函数),这时会发生的是死机,因为Windows系统在处理WM_ClOSE消息
未完成时,又被要求处理SendMessage函数,于是SendMessage函数和WM_CLOSE
消息处理过程发生了互相等待的事故.
结论是不要在线程向界面报告状态的过程中调用任何依靠消息工作的函数.
经过考查,几乎所有更新界面控件的函数如SetItemText都是依靠SendMessage
来工作的,所以会到问题的最初:"在多线程的程序中,如果线程要向界面窗口
报告状态,有两种操作方法"在这两种方法中,第二种方法是行不通的.
用VC++5.0 实 现 多 线 程 的 调 度 和 处 理一 多 任 务, 多 进 程 和 多 线 程---- Windows95 和WindowsNT 操 作 系 统 支 持 多 任 务 调 度 和 处 理, 基 于 该 功 能 所 提 供 的 多 任 务 空 间, 程 序 员 可 以 完 全 控 制 应 用 程 序 中 每 一 个 片 段 的 运 行, 从 而 编 写 高 效 率 的 应 用 程 序。 ---- 所 谓 多 任 务 通 常 包 括 这 样 两 大 类: 多 进 程 和 多 线 程。 进 程 是 指 在 系 统 中 正 在 运 行 的 一 个 应 用 程 序; 线 程 是 系 统 分 配 处 理 器 时 间 资 源 的 基 本 单 元, 或 者 说 进 程 之 内 独 立 执 行 的 一 个 单 元。 对 于 操 作 系 统 而 言, 其 调 度 单 元 是 线 程。 一 个 进 程 至 少 包 括 一 个 线 程, 通 常 将 该 线 程 称 为 主 线 程。 一 个 进 程 从 主 线 程 的 执 行 开 始 进 而 创 建 一 个 或 多 个 附 加 线 程, 就 是 所 谓 基 于 多 线 程 的 多 任 务。 ---- 开 发 多 线 程 应 用 程 序 可 以 利 用32 位Windows 环 境 提 供 的Win32 API 接 口 函 数, 也 可 以 利 用VC++ 中 提 供 的MFC 类 库 进 行 开 发。 多 线 程 编 程 在 这 两 种 方 式 下 原 理 是 一 样 的, 用 户 可 以 根 据 需 要 选 择 相 应 的 工 具。 本 文 重 点 讲 述 用VC++5.0 提 供 的MFC 类 库 实 现 多 线 程 调 度 与 处 理 的 方 法 以 及 由 线 程 多 任 务 所 引 发 的 同 步 多 任 务 特 征, 最 后 详 细 解 释 一 个 实 现 多 线 程 的 例 程。 二 基 于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 派 生 一 个 类。 简 要 说 明 如 下: ---- 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); …… 创建用户界面线程有两种方法。 ---- 第 一 种 方 法, 首 先 从CWinTread 类 派 生 一 个 类( 注 意 必 须 要 用 宏DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 对 该 类 进 行 声 明 和 实 现); 然 后 调 用 函 数AfxBeginThread 创 建CWinThread 派 生 类 的 对 象 进 行 初 始 化 启 动 线 程 运 行。 除 了 调 用 函 数AfxBeginThread 之 外, 也 可 以 采 用 第 二 种 方 法, 即 先 通 过 构 造 函 数 创 建 类CWinThread 的 一 个 对 象, 然 后 由 程 序 员 调 用 函 数::CreateThread 来 启 动 线 程。 通 常 类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 计 算 机 中 多 个 线 程 之 间 的 切 换 会 占 用 一 部 分 资 源。 三 编 程 实 例---- 下 面 以VC++5.0 中 一 个 简 单 的 基 于 对 话 框 的MFC 例 程 来 说 明 实 现 多 线 程 任 务 调 度 与 处 理 的 方 法, 下 面 加 以 详 细 解 释。 ---- 在 该 例 程 当 中 定 义 两 个 用 户 界 面 线 程, 一 个 显 示 线 程(CDisplayThread) 和 一 个 计 数 线 程(CCounterThread)。 这 两 个 线 程 同 时 操 作 一 个 字 符 串 变 量m_strNumber, 其 中 显 示 线 程 将 该 字 符 串 在 一 个 列 表 框 中 显 示, 而 计 数 线 程 则 将 该 字 符 串 中 的 整 数 加1。 在 例 程 中, 可 以 分 别 调 整 进 程、 计 数 线 程 和 显 示 线 程 的 优 先 级。 例 程 中 的 同 步 机 制 使 用CMutex 和CSingleLock 来 保 证 两 个 线 程 不 能 同 时 访 问 该 字 符 串。 同 步 机 制 执 行 与 否 将 明 显 影 响 程 序 的 执 行 结 果。 在 该 例 程 中 允 许 将 将 把 两 个 线 程 暂 时 挂 起, 以 查 看 运 行 结 果。 例 程 中 还 允 许 查 看 计 数 线 程 的 运 行。 该 例 程 中 所 处 理 的 问 题 也 是 多 线 程 编 程 中 非 常 具 有 典 型 意 义 的 问 题。 ---- 在 该 程 序 执 行 时 主 要 有 三 个 用 于 调 整 优 先 级 的 组 合 框, 三 个 分 别 用 于 选 择 同 步 机 制、 显 示 计 数 线 程 运 行 和 挂 起 线 程 的 复 选 框 以 及 一 个 用 于 显 示 运 行 结 果 的 列 表 框。 ---- 在 本 程 序 中 使 用 了 两 个 线 程 类CCounterThread 和CDisplayThread, 这 两 个 线 程 类 共 同 操 作 定 义 在CMutexesDlg 中 的 字 符 串 对 象m_strNumber。 本 程 序 对 同 步 类CMutex 的 使 用 方 法 就 是 按 照 本 文 所 讲 述 的 融 入 的 方 法 来 实 现 的。 同 步 访 问 类CSingleLock 的 锁 对 象 则 在 各 线 程 的 具 体 实 现 中 定 义。 ---- 下 面 介 绍 该 例 程 的 具 体 实 现: 1. 利 用AppWizard 生 成 一 个 名 为Mutexes 基 于 对 话 框 的 应 用 程 序 框 架。 2. 利 用 对 话 框 编 辑 器 在 对 话 框 中 填 加 以 下 内 容: 三 个 组 合 框, 三 个 复 选 框 和 一 个 列 表 框。 三 个 组 合 框 分 别 允 许 改 变 进 程 优 先 级 和 两 个 线 程 优 先 级, 其ID 分 别 设 置 为:IDC_PRIORITYCLASS、IDC_DSPYTHRDPRIORITY 和IDC_CNTRTHRDPRIORITY。 三 个 复 选 框 分 别 对 应 着 同 步 机 制 选 项、 显 示 计 数 线 程 执 行 选 项 和 暂 停 选 项, 其ID 分 别 设 置 为IDC_SYNCHRONIZE、IDC_SHOWCNTRTHRD 和IDC_PAUSE。 列 表 框 用 于 显 示 线 程 显 示 程 序 中 两 个 线 程 的 共 同 操 作 对 象m_strNumber, 其ID 设 置 为IDC_DATABOX。 3. 创 建 类CWinThread 的 派 生 类CExampleThread。 该 类 将 作 为 本 程 序 中 使 用 的 两 个 线 程 类:CCounterThread 和CDisplayThread 的 父 类。 这 样 做 的 目 的 仅 是 为 了 共 享 两 个 线 程 类 的 共 用 变 量 和 函 数。 ---- 在CExampleThread 的 头 文 件 中 填 加 如 下 变 量: CMutexesDlg * m_pOwner;//指向类CMutexesDlg指针 BOOL m_bDone;//用以控制线程执行 及函数: void SetOwner(CMutexesDlg* pOwner) { m_pOwner=pOwner; };//取类CMutexesDlg的指针 然后在构造函数当中对成员变量进行初始化: m_bDone=FALSE;//初始化允许线程运行 m_pOwner=NULL;//将该指针置为空 m_bAutoDelete=FALSE;//要求手动删除线程对象 4. 创 建 两 个 线 程 类CCounterThread 和CdisplayThread。 这 两 个 线 程 类 是CExampleThread 的 派 生 类。 分 别 重 载 两 个 线 程 函 数 中 的::Run() 函 数, 实 现 各 线 程 的 任 务。 在 这 两 个 类 当 中 分 别 加 入 同 步 访 问 类 的 锁 对 象sLock, 这 里 将 根 据 同 步 机 制 的 复 选 与 否 来 确 定 是 否 控 制 对 共 享 资 源 的 访 问。 不 要 忘 记 需 要 加 入 头 文 件#include "afxmt.h"。 ---- 计 数 线 程::Run() 函 数 的 重 载 代 码 为: int CCounterThread::Run() { BOOL fSyncChecked;//同步机制复选检测 unsigned int nNumber;//存储字符串中整数
if (m_pOwner == NULL) return -1; //将同步对象同锁对象联系起来 CSingleLock sLock(&(m_pOwner- >m_mutex)); while (!m_bDone)//控制线程运行,为终止线程服务 { //取同步机制复选状态 fSyncChecked = m_pOwner- > IsDlgButtonChecked(IDC_SYNCHRONIZE); //确定是否使用同步机制 if (fSyncChecked) sLock.Lock(); //读取整数 _stscanf((LPCTSTR) m_pOwner- >m_strNumber, _T("%d"), &nNumber); nNumber++;//加1 m_pOwner- >m_strNumber.Empty();//字符串置空 while (nNumber != 0) //更新字符串 { m_pOwner- >m_strNumber += (TCHAR) ('0'+nNumber%10); nNumber /= 10; } //调整字符串顺序 m_pOwner- >m_strNumber.MakeReverse(); //如果复选同步机制,释放资源 if (fSyncChecked) sLock.Unlock(); //确定复选显示计数线程 if (m_pOwner- >IsDlgButtonChecked(IDC_SHOWCNTRTHRD)) m_pOwner- >AddToListBox(_T("Counter: Add 1")); }//结束while
m_pOwner- >PostMessage(WM_CLOSE, 0, 0L); return 0; } 显示线程的::Run()函数重载代码为: int CDisplayThread::Run() { BOOL fSyncChecked; CString strBuffer;
ASSERT(m_pOwner != NULL); if (m_pOwner == NULL) return -1; CSingleLock sLock(&(m_pOwner- >m_mutex)); while (!m_bDone) { fSyncChecked = m_pOwner- > IsDlgButtonChecked(IDC_SYNCHRONIZE); if (fSyncChecked) sLock.Lock(); //构建要显示的字符串 strBuffer = _T("Display: "); strBuffer += m_pOwner- >m_strNumber; if (fSyncChecked) sLock.Unlock(); //将字符串加入到列表框中 m_pOwner- >AddToListBox(strBuffer); }//结束while m_pOwner- >PostMessage(WM_CLOSE, 0, 0L); return 0; }
3在CMutexesDlg的头文件中加入如下成员变量: CString m_strNumber;//线程所要操作的资源对象 CMutex m_mutex;//用于同步机制的互斥量 CCounterThread* m_pCounterThread;//指向计数线程的指针 CDisplayThread* m_pDisplayThread;//指向显示线程的指针 首先在对话框的初始化函数中加入如下代码对对话框进行初始化: BOOL CMutexesDlg::OnInitDialog() { …… //初始化进程优先级组合框并置缺省为 NORMAL CComboBox* pBox; pBox = (CComboBox*) GetDlgItem(IDC_PRIORITYCLASS); ASSERT(pBox != NULL); if (pBox != NULL){ pBox- >AddString(_T("Idle")); pBox- >AddString(_T("Normal")); pBox- >AddString(_T("High")); pBox- >AddString(_T("Realtime")); pBox- >SetCurSel(1); } //初始化显示线程优先级组合框并置缺省为 NORMAL pBox = (CComboBox*) GetDlgItem(IDC_DSPYTHRDPRIORITY); ASSERT(pBox != NULL); if (pBox != NULL){ pBox- >AddString(_T("Idle")); pBox- >AddString(_T("Lowest")); pBox- >AddString(_T("Below normal")); pBox- >AddString(_T("Normal")); pBox- >AddString(_T("Above normal")); pBox- >AddString(_T("Highest")); pBox- >AddString(_T("Timecritical")); pBox- >SetCurSel(3); } //初始化计数线程优先级组合框并置缺省为 NORMAL pBox = (CComboBox*) GetDlgItem(IDC_CNTRTHRDPRIORITY); ASSERT(pBox != NULL); if (pBox != NULL){ pBox- >AddString(_T("Idle")); pBox- >AddString(_T("Lowest")); pBox- >AddString(_T("Below normal")); pBox- >AddString(_T("Normal")); pBox- >AddString(_T("Above normal")); pBox- >AddString(_T("Highest")); pBox- >AddString(_T("Timecritical")); pBox- >SetCurSel(3); } //初始化线程挂起复选框为挂起状态 CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE); pCheck- >SetCheck(1); //初始化线程 m_pDisplayThread = (CDisplayThread*) AfxBeginThread(RUNTIME_CLASS(CDisplayThread), THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); m_pDisplayThread- >SetOwner(this);
m_pCounterThread = (CCounterThread*) AfxBeginThread(RUNTIME_CLASS(CCounterThread), THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); m_pCounterThread- >SetOwner(this); …… } 然后填加成员函数: void AddToListBox(LPCTSTR szBuffer);//用于填加列表框显示 该函数的实现代码为: void CMutexesDlg::AddToListBox(LPCTSTR szBuffer) { CListBox* pBox = (CListBox*) GetDlgItem(IDC_DATABOX); ASSERT(pBox != NULL); if (pBox != NULL){ int x = pBox- >AddString(szBuffer); pBox- >SetCurSel(x); if (pBox- >GetCount() > 100) pBox- >DeleteString(0); } } ---- 然 后 利 用ClassWizard 填 加 用 于 调 整 进 程 优 先 级、 两 个 线 程 优 先 级 以 及 用 于 复 选 线 程 挂 起 的 函 数。 ---- 调 整 进 程 优 先 级 的 代 码 为: void CMutexesDlg::OnSelchangePriorityclass() { DWORD dw; //取焦点选项 CComboBox* pBox = (CComboBox*) GetDlgItem(IDC_PRIORITYCLASS); int nCurSel = pBox- >GetCurSel(); switch (nCurSel) { case 0: dw = IDLE_PRIORITY_CLASS;break; case 1: default: dw = NORMAL_PRIORITY_CLASS;break; case 2: dw = HIGH_PRIORITY_CLASS;break; case 3: dw = REALTIME_PRIORITY_CLASS;break; } SetPriorityClass(GetCurrentProcess(), dw);//调整优先级 } ---- 由 于 调 整 两 个 线 程 优 先 级 的 代 码 基 本 相 似, 单 独 设 置 一 个 函 数 根 据 不 同 的ID 来 调 整 线 程 优 先 级。 该 函 数 代 码 为: void CMutexesDlg::OnPriorityChange(UINT nID) { ASSERT(nID == IDC_CNTRTHRDPRIORITY || nID == IDC_DSPYTHRDPRIORITY); DWORD dw; //取对应该ID的焦点选项 CComboBox* pBox = (CComboBox*) GetDlgItem(nID); int nCurSel = pBox- >GetCurSel(); switch (nCurSel) { case 0: dw = (DWORD)THREAD_PRIORITY_IDLE;break; case 1: dw = (DWORD)THREAD_PRIORITY_LOWEST;break; case 2: dw = (DWORD)THREAD_PRIORITY_BELOW_NORMAL;break; case 3: default: dw = (DWORD)THREAD_PRIORITY_NORMAL;break; case 4: dw = (DWORD)THREAD_PRIORITY_ABOVE_NORMAL;break; case 5: dw = (DWORD)THREAD_PRIORITY_HIGHEST;break; case 6: dw = (DWORD)THREAD_PRIORITY_TIME_CRITICAL;break; } if (nID == IDC_CNTRTHRDPRIORITY) m_pCounterThread- >SetThreadPriority(dw); //调整计数线程优先级 else m_pDisplayThread- >SetThreadPriority(dw); //调整显示线程优先级 } 这样线程优先级的调整只需要根据不同的ID来调用该函数: void CMutexesDlg::OnSelchangeDspythrdpriority() { OnPriorityChange(IDC_DSPYTHRDPRIORITY);} void CMutexesDlg::OnSelchangeCntrthrdpriority() { OnPriorityChange(IDC_CNTRTHRDPRIORITY);} 复选线程挂起的实现代码如下: void CMutexesDlg::OnPause() { //取挂起复选框状态 CButton* pCheck = (CButton*)GetDlgItem(IDC_PAUSE); BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0); if (bPaused) { m_pCounterThread- >SuspendThread(); m_pDisplayThread- >SuspendThread(); }//挂起线程 else { m_pCounterThread- >ResumeThread(); m_pDisplayThread- >ResumeThread(); }//恢复线程运行 } ---- 程 序 在::OnClose() 中 实 现 了 线 程 的 终 止。 在 本 例 程 当 中 对 线 程 的 终 止 稍 微 复 杂 些。 需 要 注 意 的 是 成 员 变 量m_bDone 的 作 用, 在 线 程 的 运 行 当 中 循 环 检 测 该 变 量 的 状 态, 最 终 引 起 线 程 的 退 出。 这 样 线 程 的 终 止 是 因 为 函 数 的 退 出 而 自 然 终 止, 而 非 采 用 强 行 终 止 的 方 法, 这 样 有 利 于 系 统 的 安 全。 该 程 序 中 使 用 了PostMessage 函 数, 该 函 数 发 送 消 息 后 立 即 返 回, 这 样 可 以 避 免 阻 塞。 其 实 现 的 代 码 为: void CMutexesDlg::OnClose() { int nCount = 0; DWORD dwStatus; //取挂起复选框状态 CButton* pCheck = (CButton*) GetDlgItem(IDC_PAUSE); BOOL bPaused = ((pCheck- >GetState() & 0x003) != 0); if (bPaused == TRUE){ pCheck- >SetCheck(0);//复选取消 m_pCounterThread- >ResumeThread(); //恢复线程运行 m_pDisplayThread- >ResumeThread(); }
if (m_pCounterThread != NULL){ VERIFY(::GetExitCodeThread(m_pCounterThread- > m_hThread, &dwStatus));//取计数线程结束码 if (dwStatus == STILL_ACTIVE){ nCount++; m_pCounterThread- >m_bDone = TRUE; }//如果仍为运行状态,则终止 else{ delete m_pCounterThread; m_pCounterThread = NULL; }//如果已经终止,则删除该线程对象 } if (m_pDisplayThread != NULL){ VERIFY(::GetExitCodeThread(m_pDisplayThread- > m_hThread, &dwStatus));//取显示线程结束码 if (dwStatus == STILL_ACTIVE){ nCount++; m_pDisplayThread- >m_bDone = TRUE; }//如果仍为运行状态,则终止 else{ delete m_pDisplayThread; m_pDisplayThread = NULL; }//如果已经终止,则删除该线程对象 } if (nCount == 0)//两个线程均终止,则关闭程序 CDialog::OnClose(); else //否则发送WM_CLOSE消息 PostMessage(WM_CLOSE, 0, 0); } ---- 在 例 程 具 体 实 现 中 用 到 了 许 多 函 数, 在 这 里 不 一 一 赘 述, 关 于 函 数 的 具 体 意 义 和 用 法, 可 以 查 阅 联 机 帮 助。 |
----多任务、多进程和多线程
----Windows95和WindowsNT操作系统支持多任务调度和处理,由此提供了多任务空间。程序员可控制应用程序中每一个片段的运行,从而编写高效率的应用程序。
----所谓多任务通常包括两大类:多进程和多线程。进程是指在系统中正在运行的一个应用程序;线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,这就是所谓基于多线程的多任务。
----开发多线程应用程序可利用32位Windows环境提供的Win32API接口函数,也可利用VC++中提供的MFC类库。多线程编程在这两种方式下原理是一样的,用户可以根据需要选择相应的工具。本文重点讲述用VC++5提供的MFC类库实现多线程调度与处理的方法,以及由线程多任务所引发的同步多任务特征,并给出一个实现多线程的例程。
----基于MFC的多线程编程
----1.MFC对多线程的支持
----MFC类库提供了多线程编程支持,使用户编程更加方便。重要的是,在多窗口线程情况下,MFC直接提供了用户接口线程的设计。
----MFC区分两种类型的线程:辅助线程(WorkerThread)和用户界面线程(UserInterfaceThread)。辅助线程没有消息机制,通常用来执行后台计算和维护任务。MFC为用户界面线程提供消息机制,用来处理用户的输入,响应用户产生的事件和消息。但对于Win32的API来说,这两种线程并没有区别,它只需要线程的启动地址以便启动线程执行任务。用户界面线程的一个典型应用就是CWinApp类,它是CWinThread类的派生类,提供应用程序的主线程,并负责处理用户产生的事件和消息。CWinThread类是用户接口线程的基本类,其对象用以维护特定线程的局部数据。因为处理线程局部数据依赖于CWinThread类,所以所有使用MFC的线程都必须由MFC来创建。例如,由run-time函数_beginthreadex创建的线程就不能使用任何MFCAPI。
----2.辅助线程和用户界面线程的创建和终止
----要创建一个线程,需要调用函数AfxBeginThread。该函数因参数重载不同而具有两种版本,分别对应辅助线程和用户界面线程。无论是辅助线程还是用户界面线程,都需要指定额外的参数以修改优先级、堆栈大小、创建标志和安全特性等。函数AfxBeginThread返回指向CWinThread类对象的指针。
----创建助手线程相对简单,只需实现控制函数和启动线程,而不必从CWinThread派生一个类。简要说明如下:
----(1)实现控制函数。控制函数定义该线程。当进入该函数,线程启动;退出时,线程终止。该控制函数声明如下:
----UINTMyControllingFunction(LPVOIDpParam);
----该参数是一个单精度32位值。该参数接收的值将在线程对象创建时传递给构造函数,控制函数将用某种方式解释该值。它可以是数量值,或是指向包括多个参数的结构的指针,甚至可以忽略。如果该参数是指结构,则不仅可以将数据从调用函数传给线程,也可以从线程回传给调用函数。如果使用这样的结构回传数据,当结果准备好时,线程要通知调用函数;当函数结束时,应返回一个UINT类型的值,指明结束的原因。通常,返回0表明成功,其他值分别代表不同的错误。
----(2)启动线程。由函数AfxBeginThread创建并初始化一个CWinThread类的对象,启动并返回该线程的地址,则线程进入运行状态。
----下面用简单的代码说明怎样定义一个控制函数以及如何在程序的其他部分使用。
UINTMyThreadProc(LPVOIDpParam)
{
CMyObject*pObject=(CMyObject*)pParam;
if(pObject==NULL||!pObject->IsKindOf(RUNTIME_CLASS(CMyObject)))
return-1;//非法参数
……//具体实现内容
return0;//线程成功结束
}
//在程序中调用线程的函数
……
pNewObject=newCMyObject;
AfxBeginThread(MyThreadProc,pNewObject);
……
----创建用户界面线程有两种方法。第一种方法,首先从CWinTread类派生一个类(注意:必须要用宏DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE对该类进行声明和实现);然后调用函数AfxBeginThread创建CWinThread派生类的对象进行初始化,启动线程运行。第二种方法,先通过构造函数创建类CWinThread的一个对象,然后由程序员调用函数::CreateThread来启动线程。通常CWinThread类的对象在该线程的生存期结束时将自动终止,如果程序员希望自己来控制,则需要将m_bAutoDelete设为FALSE。这样在线程终止之后,CWinThread类对象仍然存在,此时需要手动删除CWinThread对象。
----通常线程函数结束之后,线程将自行终止,CWinThread类将为我们完成结束线程的工作。如果在线程的执行过程中程序员希望强行终止线程的话,则需要在线程内部调用AfxEndThread(nExitCode),其参数为线程结束码。这样将终止线程的运行,并释放线程所占用的资源。如果从另一个线程来终止该线程,则必须在两个线程之间设置通信方法。如果从线程外部来终止线程,还可以使用Win32函数(CWinThread类不提供该成员函数):BOOLTerminateThread(HANDLEhThread,DWORDdwExitcode)。但在实际程序设计中对该函数的使用一定要谨慎,因为一旦该命令发出,将立即终止该线程,并不释放线程所占用的资源,这样可能会引起系统不稳定。
----如果所终止的线程是进程内的最后一个线程,则在该线程终止之后进程也相应终止。
----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计算机中多个线程之间的切换会占用一部分资源。
----编程实例
----本文以VC++5中一个简单的基于对话框的MFC例程为例来说明实现多线程任务调度与处理的方法。
----该例程定义两个用户界面线程、一个显示线程(CDisplayThread)和一个计数线程(CCounterThread)。这两个线程同时操作一个字符串变量m_strNumber,其中显示线程将该字符串在一个列表框中显示,而计数线程则将该字符串中的整数加1。在例程中,可以分别调整进程、计数线程和显示线程的优先级。例程中的同步机制使用CMutex和CSingleLock来保证两个线程不能同时访问该字符串,同步机制执行与否将明显影响程序的执行结果。在该例程中允许把两个线程暂时挂起,以查看运行结果。例程中还允许查看计数线程的运行。该例程中所处理的问题也是多线程编程中非常具有典型意义的问题。
----该程序主要有三个用于调整优先级的组合框,分别用于选择同步机制、显示计数线程运行、挂起线程的复选框,以及显示运行结果的列表框。
----在本程序中使用了两个线程类CCounterThread和CDisplayThread,共同操作定义在CMutexesDlg中的字符串对象m_strNumber。本程序对同步类CMutex的使用方法就是按照本文所讲述的融入的方法来实现的。同步访问类CSingleLock的锁对象则在各线程的具体实现中定义。
----:程序的具体实现方法及代码发表在计算机世界WWW站点上,地址在http://www.computerworld.com.cn/98/skill/default.htm。
问题的提出
笔者最近在开发基于Internet网上的可视电话过程中碰到了这样一个问题,即在基于In ternet网上的可视电话系统中,同时要进行语音采集、语音编译码、图像采集、图像编译码、语音和图像码流的传输,所有这些工作,都要并行处理。特别是语音信号,如果进行图像编解码时间过长,语音信号得不到服务,通话就有间断;如果图像或语音处理时间过长,而不能及时传输码流数据,通信同样也会中断。这样就要求我们实现一种并行编程,在只有一个CPU的机器上,也就是要将该CPU时间按时一定的优先准则分配给各个事件,定期处理各事件,而不会对某一事件处理过长。在32位Windows95或Windows NT下,我们可以用多线程的处理技术来实现这种并行处理。实际上,这种并行编程在很多场合下都是必须的。例如,在File Manager拷贝文件时,它显示一个对话框中包含了一个Cancel按钮。如果在文件拷贝过程中,点中Cance l按钮,就会终止拷贝。在16位Winows中,实现这类功能需要在File Copy循环内部周期性地调用PeekMessage函数。如果正在读一个很大的动作;如果从软盘读文件,则要花费好几秒的时间。由于机器反应太迟钝,用户会频繁地点中这个按钮,以为系统不知道想终止这个操作。如果把File Copy指令放入另外一个线程,就不需要在代码中放一大堆PeekMessage函数,处理用户界面的线程将与它分开操作,点中Cancel按钮后会立即得到响应。同样的道理,在应用程序中创建一个单独线程来处理所有打印任务也是很有用的,用户可以在打印处理时继续使用应用程序。
线程的概念
为了了解线程的概念 ,我们必须先讨论一下进程的概念。一个进程通常定义为程序的一个实例。在32位Windows中,进程占据4GB的虚拟地址空间。与它们在MS-DOS和16位Windows操作系统中不同,32位Windows进程是没有活力的。这就是说,一个32位Windows进程并不执行什么指令,它只是占据着4GB的地址空间,此空间中有应用程序EXE文件的代码和数据。
EXE需要的DLL也将它们的代码的数据装入到进程的地址空间。除了地址空间,进程还占有某些资源,比如文件、动态内存分配和线程。当进程终止时,在它生命期中创建的各种资源将被清除。
如上所述,进程是没有活力的,它只是一个静态的概念。为了让进程完成一些工作,进程必须至少占有一线程,所以线程是描述进程内的执行,正是线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含几个线程,它们可以同时执行进程的地址空间中的代码。为了做到这一点,每个线程有自己的一组CPU寄存器和椎。每个进程至少有一个线址程在执行其地址空间中的代码,如果没有线程执行进程地空间中的代码,如果没有线程执行进程地址空间中的代码,进程也就没有继续存在的理由,系统将自动清除进程及其地址空间。为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮转方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。创建一个32位Windows进程时,它的第一个线程称为主线程,由系统自动生成,然后可由这个主线程生成额外的线程,这些线程又可生成更多的线程。
线程的编程技术
1.编写线程函数
所有线程必须从一个指定的函数开始执行,该函数称为线程函数,它必须具有下列原型: DWORD WINAPI YourThreadFunc(LPVOID lpvT.hreadParm);
该函数输入一个LPVOID型的参数,可以是一个DWORD型的整数,也可以是一个指向一个缓冲区的指针,返回一个DWORD型的值。像WinMain函数一样,这个函数并不由操作系统调用,操作系统调用包含在KERNEL32.DLL中的非C运行时的一个内部函数,如StartOfThread,然后由S tartOfThread函数建立起一个异常处理框架后,调用我们的函数。
2.创建一个线程
一个进程的主线程是由操作系统自动生成,如果要让一个主线程创建额外的线程,可以调用CreateThread来完成。格式如下:
HANDLE CreateThread(LPSECURITY_ATTRIBUTES jpsa.DWORD cbstack,LPTHREAD_START _ROUTINE lpStartAddr.LPVOID lpvThreadParm,DWORD fdwCreate,LPDWORD lpIDThread);
其中参数意义如下:
lpsa:是一个指向SECURITY_ATTRIBUTES结构的指针。如果想让对象为缺省安全属性的话,可以传一个NULL;如果想让任一个子进程都可继承该线程对象句柄,必须指定一个SECURITY _ATTRIBUTES结构,其中bInheritHandle成员初始化为TURE。
cbstark:表示线程为自己所用堆栈分配的地址空间大小,0表示采用系统缺省值。
lpStartAddr:表示新线程开始执行时代码所在函数的地址,即为线程函数。
lpvThreadParm:是传入线程函数的参数。
fdwCreate:指定控制线程创建的附加标志,可以取两种值。如果该参数为0,线程就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,初始化CPU,登记CONTEXT结构的成员,准备好执行该线程函数中的第一条指令,但并不马上执行,而是挂起该线程。
lpIDThrdad:是一个DWORD类型地址,返回赋给该新线程的ID值。
3.终止线程
如果某线程调用了ExitThread函数,就可以终止自己,如:
VOID ExtThead(UNIT fuExitCode);
这个函数为调用该函数的线程设置了退出码fuExitCode后,就终止该线程。
调用TerminateThread函数亦可终止线程。如:
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
该函数用来结束由hThread参数指定的线程,并把dwExitCode设成该线程的退出码。
当某个线程不再响应时,我们可以用其他线程调用该函数来终卡这个不响应的线程。
4.设定线程的相对优先级
当一个线程被首次创建时,它的优先级等同于它所属进程的优先级。在单个进程内可以通过调用SetThreadPrionrity函数改变线程的相对优先级。一个线程的优先级是相对于其所属的进程优先级而言的。
BOOL SetThreadPriority(HANDLE hThread,intnPriority);
其中参数hThread是指向待修改优先级线程的句柄,nPriority可以是以下的值:
THREAD_PRIORITY_LOWEST THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIONRITY_ABOVE_NORMAL THREAD_PRIONITY_HIGHEST。
5.挂起及恢复线程
前文提到过可以创建挂起状态的线程,可以通过传递(CREATE_SUSPENDED标志给函数Cre ated来实现。当这样操作时,系统创建指定线程的核心对象,创建线程的栈,在CONTEXT结构中初始化线程CPU注册成员。然而,线程对象被分配了一个初始挂起计数值1,这表明系统将不再分配CPU去执行线程。要开始执行一个线程,另一个线程必须调用ResumeThread并传递给它调用CreateThread时返回的线程句柄。格式如下:
DWORD ResumeThread(HANDLE hThread);
一个线程可以被挂起多次。如果一个线程被挂起3次,则该线程在它被分配CPU之前必须被恢复3次。除了在创建线程时使用CREATE_SUSPENDED标志,还可以用SuspendThread函数挂起线程。格式如下:
DWORD SuspendThread(HANDLE hThread)。
多线程编程技术的应用
如前所述,为了实现基于TCP/IP下的可视电话,就必须"并行"地执行语音采集、语音编解码、图像采集、图像编解以及码流数据的接收与发送。语音与图像的采集由硬件采集卡进行,我们程序只需初始化该硬件采集卡,然后实时读取采集数据即可,但语音和图像数据的编解码以及码流数据的传输都必须由程序去协调执行,让CPU轮流为各个事件服务,决不能在某一件事件上处理过长。Windows 95下的线程正是满足这种要求的编程技术。
本文给出了利用Windows 95环境下用多线程编程技术实现的基于TCP/IP的可视电话的部分源码,其中包括主窗口过程函数以及主叫端与被叫端的TCP/IP接收线程函数和语音编解码的线程函数。由于图像编解码的实时性比语音处理与传输模块的实时性的要求要低些,所以以语音编解码为事件去查图像数据,然后进行图像编解码,而没有为图像编解码去单独实现一个线程。
在主窗口初始化时,用CREATE_SUSPENDED标志创建了两个线程hThreadG7231和hThreadT CPRev。一个用于语音编解码,它的线程函数为G723Proc,该线程不断查询本地有无编好码的语音和图像的码流,如有,则进行H.223打包,然后通过TCP的端口发送给对方。另外一个线程用于TCP/IP的接收,它的线程函数为AcceptThreadProcRiv,该线程不断侦测TCP/IP端口有无对方传来的码流,如有,就接收码流,进行H.223解码后送入相应的缓冲区。该缓冲区的内容, 由语音编解码线程G723Proc查询,并送入相应的解码器,由于使用了多线程的编程技术,使得操作系统定时去服务语音编解码模块和传输模块,从而保证了通信的不中断。
编者注:源程序发表在本报的WWW站点上,网址是:http://www.computerworld.co.cn/sk ill./skill.html,欢迎广大读者访问。
多线程编程应该注意的问题 从线程应用程序比单线程应用程序处理起来要更小心,因为是多个,每个线程都有自己的执行路线,不好控制,容易出问题。下面我们就看看在使用MFC进行多线程编程中应该注意的问题。 出于所占空间和效率的问题,MFC对象只在对象一级是安全的。你可以让两个线程处理两个数组,但不能让两个线程处理同一个数组,这可能就要出问题了。如果你必须使多个线程共同处理一个对象,必须使用适当的Win32同步机制。这方面的内容请参阅其它资料。类库使用临界区内部保护全局数据结束,如那些调试时使用的内存。如果你的应用程序中使用的线程不是使用CWinThread得到的,那就不能那个线程访问其它的MFC对象。 作为一个通用规则,线程只能访问它创建的MFC对象,这是因为临时的和永久的Windows句柄映射保存于线程本地存储中,这些句柄可以在多个线程访问数据时进行同步机制的保护。例如,工作线程不能执行一个计算,它调用文档的UpdateAllViews成员函数来更新所有的视,可是这并不能起作用,因为从CWnd对象到HWND的映射对主线程是局部的。这也就是说,一个线程可以拥有一个从Windows句柄到C++对象的映射,但不同的线程可以将同一个句柄映射到不同的C++对象,在一个线程中的改变不会影响到其它线程。 解决这个问题有几个办法,一个是传送进一个单独的句柄,而不是一个C++对象进工作线程。工作线程通过调用适当的FromHandle函数将这些对象加载到它的临时映射中去。当然也可以通过调用Attach加对象加到永久映射中去,但是这时你必须能够保证这个对象的生存时间比线程要长。 另一个办法是创建新的用户自定义消息,不同的线程使用不同的消息通知主程序什么事情发生了。 |
◆ 多线程简介
线程(thread)是操作系统分配 CPU 时间的基本实体。每一个应用程序至少有一个线程,也可以拥有多个线程。线程是程序中的代码流。多个线程可以同时运行,并能共享资源。
线程与进程不同,每个进程都需要操作系统为其分配独立的地址空间。而同一进程中的各个线程是在同一块地址空间中工作。
在 Java 程序中,一些动态效果(如动画的实现、动态的字幕等)常利用多线程技术来实现。
在 Java 语言中可以非常方便地使用多线程。和 Visual C++ 相比,Java 的多线程既容易学习,又容易使用。
◆ 创建多线程的两种办法:
(1)建立类 Thread 的子类
(2)实现接口 Rinnable
第二个办法比第一个使用得更为广泛。本讲座重点讲解第二个办法。
◆ 接口
Java 语言取消了 C++ 的多重继承(“多重继承”常常使 C++ 程序员陷入混乱之中)。Java 增加了“接口”(interface)的概念,使 Java 在取消多重继承后,并未使功能下降。
“接口”(interface)是一种特殊的类。当你定义一个类时,可以“实现”(implements)一个(或多个)接口。语法如下:
class 类名 extends 超类名 implements 接口名
◆ 例 1.5.1 一个最简单的多线程小应用程序
import java.applet.*;
import java.awt.*;
public class k04a extends Applet implements Runnable
{
private Thread m_k04a = null;
public k04a()
{
}
public void paint(Graphics g)
{
g.drawString("Running: " + Math.random(), 10, 20);
}
public void start()
{
m_k04a = new Thread(this);
m_k04a.start();
}
public void stop()
{
m_k04a.stop();
m_k04a = null;
}
public void run()
{
while (true)
{
try
{
repaint();
Thread.sleep(200);
}
catch (InterruptedException e)
{
stop();
}
}
}
}
◆ 控制线程的生命周期
(1)start()方法 启动一个线程
(2)run()方法 定义该线程的动作
(3)sleep()方法 使线程睡眠一段时间,单位为毫秒
(4)suspend()方法 使线程挂起
(5)resume()方法 恢复挂起的线程
(6)yield()方法 把线程移到队列的尾部
(7)stop()方法 结束线程生命周期并执行清理工作
(8)destroy()方法 结束线程生命周期但不做清理工作
其中最常用的是start(),run(),sleep(),stop()。
◆ try —— catch 语句
用于对“异常”的处理。和“错误”相比,“异常”是比较轻微的。它是指程序在运行中发生的意外情况。(try - catch 语句在 C++ 中也有)。
在执行 try 后面的语句时,如果发生异常,则执行 catch 后面的语句。
◆ 例 1.5.2 流动的标题
该程序在运行时,三个标题在由下而上不断变换。
HTML 文件中的写法:(三个图片要事先做好)
JAVA 源程序:
import java.awt.*;
import java.applet.Applet;
public class testani extends Applet implements Runnable
{
Thread runner;
Image imgs[];
int high, y1, y2, y3;
public void init()
{
high = size().height;
y1 = high;
y2 = high*2;
y3 = high*3;
imgs = new Image[10];
for(int i=0;i<3;i++)
imgs[i]=getImage(getCodeBase(),getParameter("image"+(i+1)));
}
public void start()
{
runner = new Thread(this);
runner.start();
}
public void stop()
{
runner.stop();
runner = null;
}
public void run()
{
while (runner != null)
{
try
{
Thread.sleep(100);
repaint();
y1--;
if(y1==0)
{
Thread.sleep(3000);
y2=high;
}
y2--;
if(y2==0)
{
Thread.sleep(3000);
y3=high;
}
y3--;
if(y3==0)
{
Thread.sleep(3000);
y1 = high;
}
}
catch (InterruptedException e){}
}
}
public void paint(Graphics g)
{
g.drawImage(imgs[0], 0, y1, this);
g.drawImage(imgs[1], 0, y2, this);
g.drawImage(imgs[2], 0, y3, this);
}
public void update(Graphics g)
{
paint(g);
}
}
Visual C++ 5.0中的多线程编程技术潘爱民 一、引言 Windows系统平台经历了从16位到32位的转变后,系统运行方式和任务管理方式有了很大的变化,在Windows 95和Windows NT中,每个Win32程序在独立的进程空间上运行,32位地址空间使我们从16位段式结构的64K段限制中摆脱出来,逻辑上达到了4G的线性地址空间。这样,我们在设计程序时就不再需要考虑编译的段模式,同时还提高了大程序的运行效率。独立进程空间的另一个更大的优越性是大大提高了系统的稳定性,一个应用程序的异常错误不会影响其它的应用程序,这对于现在的桌面环境尤为重要。 在Windows的一个进程内,包含一个或多个线程。线程是指进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享所有的进程资源,包括打开的文件、信号标识及动态分配的内存等等。一个进程内的所有线程使用同一个32位地址空间,而这些线程的执行由系统调度程序控制,调度程序决定哪个线程可执行以及什么时候执行线程。线程有优先级别,优先权较低的线程必须等到优先权较高的线程执行完任务后再执行。在多处理器的机器上,调度程序可将多个线程放到不同的处理器上去运行,这样就可使处理器的任务平衡,也提高了系统的运行效率。 32位Windows环境下的Win32 API提供了多线程应用程序开发所需要的接口函数,但Win16和Win32对多线程应用并不支持,利用Visual C++ 5.0中提供的标准C库也可以开发多线程应用程序,而相应的MFC4.21类库则封装了多线程编程的类,因而用户在开发时可根据应用程序的需要和特点选择相应的工具。 如果用户的应用程序需要有多个任务同时进行相应的处理,则使用多线程是较理想的选择。例如,就网络文件服务功能的应用程序而言,若采用单线程编程方法,则需要循环检查网络的连接、磁盘驱动器的状况,并在适当的时候显示这些数据,必须等到一遍查询后才能刷新数据的显示。对使用者来说,延迟可能很长。而在应用多线程的情况下可将这些任务分给多个线程,一个线程负责检查网络,另一个线程管理磁盘驱动器,还有一个线程负责显示数据,三个线程结合起来共同完成文件服务,使用者也可以及时看到网络的变化。多线程应用范围很广,尤其是在目前的桌面平台上,系统的许多功能如网络(Internet)、打印、字处理、图形图像、动画和文件管理都在一个系统下运行,更需要我们的应用程序能够同时处理多个事件,而这些正是多线程可以实现的。本文讲述了利用Visual C++ 5.0进行多线程开发的编程技术。 二、基于Visual C++的多线程编程 Visual C++ 5.0提供了Windows应用程序的集成开发环境Developer Studio。在这个环境里,用户既可以编写C风格的32位Win32应用程序,也可以利用MFC类库编写C++风格的应用程序,二者各有其优点:基于Win32的应用程序执行代码小巧,运行效率高,但要求程序员编写的代码较多,且需要管理所有系统提供给程序的资源;而基于MFC类库的应用程序可以快速建立起应用程序,类库为程序员提供了大量的封装类,而且Developer Studio为程序员提供了一些工具来管理用户源程序,其缺点是类库代码很庞大,应用程序的执行代码离不开这些代码。由于使用类库所带来的快速、简捷和功能强大等优越性,因此,除非有特殊的需要,否则Visual C++提倡使用MFC类库进行应用程序开发。 多线程的编程在Win32方式下和MFC类库支持下的原理是一致的,进程的主线程在任何需要的时候都可以创建新的线程。当线程执行完任务后,自动中止线程;当进程结束后,所有的线程都中止。所有活动的线程共享进程的资源。因此,在编程时需要考虑在多个线程访问同一资源时产生冲突的问题:当一个线程正在访问一个进程对象时,另一个线程要改变该对象,这时可能会产生错误的结果。所以,程序员编程时要解决这种冲突。 下面给大家介绍一下在Win32 基础上进行多线程编程的过程。 1.用Win32函数创建和中止线程 Win32函数库中提供了多线程控制的操作函数,包括创建线程、中止线程、建立互斥区等。首先,在应用程序的主线程或者其它活动线程的适当地方创建新的线程。创建线程的函数如下: HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId ); 其中,参数lpThreadAttributes 指定了线程的安全属性,在Windows 95中被忽略;dwStackSize 指定了线程的堆栈深度;lpStartAddress 指定了线程的起始地址,一般情况为下面的原型函数:DWORD WINAPI ThreadFunc( LPVOID );lpParameter指定了线程执行时传送给线程的32位参数,即上面函数的参数;dwCreationFlags指定了线程创建的特性; lpThreadId 指向一个DWORD变量,可返回线程ID值。 如果创建成功则返回线程的句柄,否则返回NULL。 创建了新的线程后,则该线程就开始启动执行了。如果在dwCreationFlags中用了CREATE_SUSPENDED特性,那么线程并不马上执行,而是先挂起,等到调用ResumeThread后才开始启动线程,在这个过程中可以调用函数: BOOL SetThreadPriority( HANDLE hThread, int nPriority); 来设置线程的优先权。 当线程的函数返回后,线程自动中止。如果在线程的执行过程中中止的话,则可调用函数: VOID ExitThread( DWORD dwExitCode); 如果在线程的外面中止线程的话,则可调用下面的函数: BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode ); 但应注意:该函数可能会引起系统不稳定,而且线程所占用的资源也不释放。因此,一般情况下,建议不要使用该函数。 如果要中止的线程是进程内的最后一个线程,则在线程被中止后相应的进程也应中止。 2.用Win32函数控制线程对共享资源的访问 在线程体内,如果该线程完全独立,与其它的线程没有数据存取等资源操作上的冲突,则可按照通常单线程的方法进行编程。但是,在多线程处理时情况常常不是这样,线程之间经常要同时访问一些资源。例如,一个线程负责公式计算,另一个线程负责结果的显示,两个线程都要访问同一个结果变量。这时如果不进行冲突控制的话,则很可能显示的是不正确的结果。 对共享资源进行访问引起冲突是不可避免的,但我们可用以下办法来进行操作控制: (1) 通过设置线程的互斥体对象,在可能冲突的地方进行同步控制。 首先,建立互斥体对象,得到句柄: HANDLE CreateMutex( ); 然后,在线程可能冲突区域的开始(即访问共享资源之前),调用WaitForSingleObject将句柄传给函数,请求占用互斥体对象: dwWaitResult = WaitForSingleObject(hMutex, 5000L); 共享资源访问完后,释放对互斥体对象的占用: ReleaseMutex(hMutex); 互斥体对象在同一时刻只能被一个线程占用。当互斥体对象被一个线程占用时,若有另一线程想占用它,则必须等到前一线程释放后才能成功。 (2) 设置信号:在操作共享资源前,打开信号;完成操作后,关闭信号。这类似于互斥体对象的处理。 首先,创建信号对象: HANDLE CreateSemaphore( ); 或者打开一个信号对象: HANDLE OpenSemaphore( ); 然后,在线程的访问共享资源之前调用WaitForSingleObject。 共享资源访问完后,释放对信号对象的占用: ReleaseSemaphore(); 信号对象允许同时对多个线程共享资源的访问,在创建对象时指定最大可同时访问的线程数。当一个线程申请访问成功后,信号对象中的计数器减一;调用ReleaseSemaphore函数后,信号对象中的计数器加一。其中,计数器值大于等于0,小于等于创建时指定的最大值。利用信号对象,我们不仅可以控制共享资源的访问,还可以在应用的初始化时候使用。假定一个应用在创建一个信号对象时,将其计数器的初始值设为0,这样就阻塞了其它线程,保护了资源。待初始化完成后,调用ReleaseSemaphore函数将其计数器增加至最大值,进行正常的存取访问。 (3) 利用事件对象的状态,进行线程对共享资源的访问。 用ResetEvent函数设置事件对象状态为不允许线程通过;用SetEvent函数设置事件对象状态为可以允许线程通过。 事件分为手工释放和自动释放。如果是手工释放,则按照上述两函数处理事件的状态;如果是自动释放,则在一个线程结束后,自动清除事件状态,允许其它线程通过。 (4) 设置排斥区。在排斥区中异步执行时,它只能在同一进程的线程之间共享资源处理。虽然此时上面介绍的三种方法均可使用,但是,使用排斥区的方法则使同步管理的效率更高; 先定义一个CRITICAL_SECTION结构的排斥区对象,在进程使用之前先对对象进行初始化,调用如下函数: VOID InitializeCriticalSection( LPCRITICAL_SECTION ); 当一个线程使用排斥区时,调用函数: EnterCriticalSection或者TryEnterCriticalSection 当要求占用、退出排斥区时,调用函数: LeaveCriticalSection 释放对排斥区对象的占用,供其它线程使用。 互斥体对象、信号对象和事件对象也可以用于进程间的线程同步操作。在用Win32函数创建了对象时,我们可以指定对象的名字,还可以设置同步对象在子进程的继承性。创建返回的是HANDLE句柄,我们可以用函数DuplicateHandle来复制对象句柄,这样每个进程都可以拥有同一对象的句柄,实现进程之间的线程同步操作。另外,在同一进程内,我们可以用OpenMutex、OpenSemaphore和OpenEvent来获得指定名字的同步对象的句柄。 排斥区异步执行的线程同步方法只能用于同一进程的线程之间共享资源处理,但是这种方法的使用效率较高,而且编程也相对简单一些。 在Visual C++中,除了利用Win32函数进行多线程同步控制外,如果我们用到了MFC类库,则可利用已经封装成C++类结构的同步对象,使我们的编程更加简捷。 三、基于MFC的多线程编程 在Visual C++ 5.0附带的MFC 4.21类库中,也提供了多线程编程的支持,基本原理与上面所讲的基于Win32函数的设计一致,但由于MFC对同步对象作了封装,因此对用户编程实现来说更加方便,避免了对象句柄管理上的繁琐工作。更重要的是,在多个窗口线程情况下,MFC中直接提供了用户接口线程的设计。 在MFC中,线程分为两种:用户接口线程和辅助线程。用户接口线程常用于接收用户的输入,处理相应的事件和消息。在用户接口线程中,包含一个消息处理循环,其中CWinApp就是一个典型的例子,它从CWinThread派生出来,负责处理用户输入产生的事件和消息。辅助线程常用于任务处理(比如计算)不要求用户输入,对用户而言,它在后台运行。Win32 API并不区分这两种线程的类型,它只是获取线程的起始地址,然后开始执行线程。而MFC则针对不同的用户需要作了分类。如果我们需要编写多个有用户接口的线程的应用程序,则利用Win32 API要写很多的框架代码来完成每个线程的消息事件的处理,而用MFC则可以充分发挥MFC中类的强大功能,还可以使用ClassWizard来帮助管理类的消息映射和成员变量等,我们就可以把精力集中到应用程序的相关代码编写上。 辅助线程编程较为简单,设计的思路与上节所讲的基本一致:一个基本函数代表了一个线程,创建并启动线程后,则线程进入运行状态;如果线程用到共享资源,则需要进行资源同步处理。共享资源的同步处理在两种线程模式下完全一致。 我们知道:基于MFC的应用程序有一个应用对象,它是CWinApp派生类的对象,该对象代表了应用进程的主线程。当线程执行完(通常是接收到WM_QUIT消息)并退出线程时,由于进程中没有其它线程的存在,故进程也自动结束。类CWinApp从CWinThread派生出来,CWinThread是用户接口线程的基本类。我们在编写用户接口线程时,需要从CWinThread派生我们自己的线程类,ClassWizard可以帮助我们完成这个工作。 下面列出编写用户接口线程的基本步骤。 1.用ClassWizard派生一个新的类,设置基类为CWinThread 注意:类的DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE宏是必需的,因为创建线程时需要动态创建类的对象。根据需要可将初始化和结束代码分别放到类的InitInstance和ExitInstance函数中。如果需要创建窗口,则可在InitInstance函数中完成。 2.创建线程并启动线程 可以用两种方法来创建用户接口线程。 (1)MFC提供了两个版本的AfxBeginThread函数,其中一个用于创建用户接口线程,函数原型如下: CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass, int nPriority, UINT nStackSize , DWORD dwCreateFlags, LPSECURITY_ATTRIBUTES lpSecurityAttrs ); 其中,参数pThreadClass指定线程的运行类,函数返回线程对象。 在创建线程时,可以指定线程先挂起,将参数dwCreateFlags设置为CREATE_SUSPENDED。然后,做一些初试工作,如对变量赋值等。最后,再调用线程类的ResumeThread函数启动线程。 函数AfxBeginThread的另一个版本指定一个线程函数并设置相应的参数,其它设置及用法与上述函数基本相同。 (2)我们也可以不用AfxBeginThread创建线程,而是分两步完成:首先,调用线程类的构造函数创建一个线程对象;其次,调用CWinThread::CreateThread函数来创建该线程。 注意:在这种情况下,在线程类中需要有公有的构造函数以创建其相应的C++对象。 线程建立并启动后,则线程在线程函数执行过程中一直有效。如果是线程对象,则在对象被删除之前,先结束线程。CWinThread已经为我们完成了线程结束的工作。 3. 同步对象的使用 不管是辅助线程还是用户接口线程,在存取共享资源时,都需要保护共享资源,以免引起冲突,造成错误。处理方法类似于Win32 API函数的使用,但MFC为我们提供了几个同步对象C++类,即CSyncObject、CMutex、CSemaphore、CEvent、CCriticalSection。这里,CSyncObject为其它四个类的基类,后四个类分别对应前面所讲的四个Win32 API同步对象。 通常,我们在C++对象的成员函数中使用共享资源,或者把共享资源封装在C++类的内部。我们可将线程同步操作封装在对象类的实现函数当中,这样在应用中的线程使用C++对象时,就可以像一般对象一样使用它,简化了使用部分代码的编写,这正是面向对象编程的思想。这样编写的类被称作“线程安全类”。在设计线程安全类时,首先应根据具体情况在类中加入一个同步对象类数据成员。然后,在类的成员函数中,凡是所有修改公共数据或者读取公共数据的地方均要加入相应的同步调用。一般的处理步骤是:创建一个CSingleLock或者CMultiLock对象,然后调用其Lock函数。当对象结束时,自动在析构函数中调用Unlock函数,当然也可以在任何希望的地方调用Unlock函数。 如果不是在特定的C++对象中使用共享资源,而是在特定的函数中使用共享资源(这样的函数称为“线程安全函数”),那么还是按照前面介绍的办法去做:先建立同步对象,然后调用等待函数,直到可以访问资源,最后释放对同步对象的控制。 下面我们讨论四个同步对象分别适用的场合: (1)如果某个线程必须等待某些事件发生后才能存取相应资源,则用CEvent; (2)如果一个应用同时可以有多个线程存取相应资源,则用CSemaphore; (3)如果有多个应用(多个进程)同时存取相应资源,则用CMutex,否则用CCriticalSection。 使用线程安全类或者线程安全函数进行编程,比不考虑线程安全的编程要复杂,尤其在进行调试时情况更为复杂,我们必须灵活使用Visual C++提供的调试工具,以保证共享资源的安全存取。线程安全编程的另一缺点是运行效率相对要低些,即使在单个线程运行的情况下也会损失一些效率。所以,我们在实际工作中应具体问题具体分析,以选择合适的编程方法。 4. 多线程编程例程分析 上面讲述了在Visual C++ 5.0中进行多线程编程的技术要点,为了充分说明这种技术,我们来分析一下Visual C++提供的有关多线程的例程,看看一些多线程元素的典型用法。读者可运行这些例程,以获得多线程运行的直观效果。 (1)MtRecalc 例程MtRecalc的功能是在一个窗口中完成简单的加法运算,用户可输入加数和被加数,例程完成两数相加。用户可通过菜单选择单线程或用辅助线程来做加法运算。如果选择辅助线程进行加法运算,则在进行运算的过程中,用户可继续进行一些界面操作,如访问菜单、编辑数值等,甚至可以中止辅助运算线程。为了使其效果更加明显,例程在计算过程中使用了循环和延时,模拟一个复杂的计算过程。 在程序的CRecalcDoc类中,用到了一个线程对象和四个同步事件对象: CWinThread* m_pRecalcWorkerThread; HANDLE m_hEventStartRecalc; HANDLE m_hEventRecalcDone; HANDLE m_hEventKillRecalcThread; HANDLE m_hEventRecalcThreadKilled; 当用户选择了菜单项Worker Thread后,多线程功能才有效。这时,或者选择菜单项Recalculate Now,或者在窗口中的编辑控制转移焦点时,都会调用函数: void CRecalcDoc::UpdateInt1AndInt2(int n1, int n2, BOOL bForceRecalc); 在多线程的情况下,还会调用下面的CRecalcDoc::RecalcInSecondThread函数: void CRecalcDoc::RecalcInSecondThread() { if (m_pRecalcWorkerThread == NULL) { m_pRecalcWorkerThread = AfxBeginThread(RecalcThreadProc, &m_recalcThreadInfo); } m_recalcThreadInfo.m_nInt1 = m_nInt1; m_recalcThreadInfo.m_nInt2 = m_nInt2; POSITION pos = GetFirstViewPosition(); CView* pView = GetNextView(pos); m_recalcThreadInfo.m_hwndNotifyRecalcDone = pView->m_hWnd; m_recalcThreadInfo.m_hwndNotifyProgress = AfxGetMainWnd()->m_hWnd; m_recalcThreadInfo.m_nRecalcSpeedSeconds = m_nRecalcSpeedSeconds; SetEvent(m_hEventRecalcDone); ResetEvent(m_hEventKillRecalcThread); ResetEvent(m_hEventRecalcThreadKilled); SetEvent(m_hEventStartRecalc); } 上面加粗的语句是与多线程直接相关的代码,应用程序调用AfxBeginThread启动了线程,把m_recalcThreadInfo作为参数传给线程函数。函数中最后的四行语句设置了四个事件对象的状态,这些事件对象在文档类的构造函数中创建。下面是实际的运算线程函数: UINT RecalcThreadProc(LPVOID pParam) { CRecalcThreadInfo* pRecalcInfo = (CRecalcThreadInfo*)pParam; BOOL bRecalcCompleted; while (TRUE) { bRecalcCompleted = FALSE; if (WaitForSingleObject(pRecalcInfo->m_hEventStartRecalc, INFINITE) != WAIT_OBJECT_0) break; if (WaitForSingleObject(pRecalcInfo->m_hEventKillRecalcThread, 0) WAIT_OBJECT_0) break; // Terminate this thread by existing the proc. ResetEvent(pRecalcInfo->m_hEventRecalcDone); bRecalcCompleted = SlowAdd(pRecalcInfo->m_nInt1, pRecalcInfo->m_nInt2, pRecalcInfo->m_nSum, pRecalcInfo, pRecalcInfo->m_nRecalcSpeedSeconds, pRecalcInfo->m_hwndNotifyProgress); SetEvent(pRecalcInfo->m_hEventRecalcDone); if (!bRecalcCompleted) // If interrupted by kill then... break; // terminate this thread by exiting the proc. ::PostMessage(pRecalcInfo->m_hwndNotifyRecalcDone, WM_USER_RECALC_DONE, 0, 0); } if (!bRecalcCompleted) SetEvent(pRecalcInfo->m_hEventRecalcThreadKilled); return 0; }
BOOL SlowAdd(int nInt1, int nInt2, int& nResult, CRecalcThreadInfo* pInfo, int nSeconds,HWND hwndNotifyProgress) { CWnd* pWndNotifyProgress = CWnd::FromHandle(hwndNotifyProgress); BOOL bRestartCalculation = TRUE; while (bRestartCalculation) { bRestartCalculation = FALSE; for (int nCount = 1; nCount <20; nCount++) { if (pInfo != NULL && WaitForSingleObject(pInfo->m_hEventKillRecalcThread, 0) == WAIT_OBJECT_0) { if (hwndNotifyProgress != NULL) { pWndNotifyProgress->PostMessage( WM_USER_RECALC_IN_PROGRESS); } return FALSE; // Terminate this recalculation } if (pInfo != NULL &&WaitForSingleObject(pInfo->m_hEventStartRecalc, 0) == WAIT_OBJECT_0) { nInt1 = pInfo->m_nInt1; nInt2 = pInfo->m_nInt2; bRestartCalculation = TRUE; continue; } // update the progress indicator Sleep(nSeconds * 50); } // update the progress indicator } nResult = nInt1 + nInt2; return TRUE; } 上面的代码充分显示了几个事件对象的用法。当线程刚启动时,先等待m_hEventStartRecalc的状态为允许,然后检查m_hEventKillRecalcThread事件对象的状态。注意这两个等待函数调用的第二个参数的区别:在进入计算函数之前,设置m_hEventRecalcDone事件为不允许状态;待计算结束后,将其设置为允许状态。在计算函数的处理过程中,循环检查事件m_hEventKillRecalcThread和m_hEventStartRecalc的状态,如果m_hEventKillRecalcThread事件允许,则退出线程,中止计算。 当计算线程在计算时,主线程可继续接受用户输入(包括菜单选择)。用户可通过菜单项中止计算线程。中止线程的处理比较简单,代码如下: void CRecalcDoc::OnKillWorkerThread() { SetEvent(m_hEventKillRecalcThread); SetEvent(m_hEventStartRecalc); WaitForSingleObject(m_hEventRecalcThreadKilled, INFINITE); m_pRecalcWorkerThread = NULL; m_bRecalcInProgress = FALSE; // but m_bRecalcNeeded is still TRUE UpdateAllViews(NULL, UPDATE_HINT_SUM); } 通过设置m_hEventKillRecalcThread事件对象,计算线程的循环就会检测到该事件的状态,最终引起线程的退出。注意:线程的中止因函数的退出而自然中止,而没有用强行办法中止,这样可保证系统的安全性。另外,在程序的很多地方使用了PostMessage来更新计算进度的指示,使用PostMessage函数发送消息可立即返回,无需等待,这样就避免了阻塞,比较符合多线程编程的思想,建议读者使用这种消息发送方法。尤其是在多个UI线程编程时,用这种方法更合适。 (2)MtMDI 例程MtMDI是一个MDI应用,每一个子窗口是一个用户接口线程,子窗口里有一个来回弹跳的小球,小球的运动由计时器控制,此处不加以讨论。下面,我们来看看UI线程的创建过程以及它与MDI的结合。 通过菜单命令New Bounce,可在主框架窗口类中响应菜单命令,函数代码如下: void CMainFrame::OnBounce() { CBounceMDIChildWnd *pBounceMDIChildWnd = new CBounceMDIChildWnd; if (!pBounceMDIChildWnd->Create( _T("Bounce"), WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, rectDefault, this)) return; } 函数调用子框架窗口的创建函数,代码如下: BOOL CBounceMDIChildWnd::Create(LPCTSTR szTitle, LONG style, const RECT& rect, CMDIFrameWnd* parent) { // Setup the shared menu if (menu.m_hMenu == NULL) menu.LoadMenu(IDR_BOUNCE); m_hMenuShared = menu.m_hMenu; if (!CMDIChildWnd::Create(NULL, szTitle, style, rect, parent)) return FALSE; CBounceThread* pBounceThread = new CBounceThread(m_hWnd); pBounceThread->CreateThread(); return TRUE; } 当CBounceMDIChildWnd子窗口被删除时,Windows会同时删除CBounceWnd窗口(内嵌在线程对象pBounceThread中),因为它是CBounceMDIChildWnd的子窗口。由于CBounceWnd运行在单独的线程中,故当CBounceWnd子窗口被删除时,CWinThread线程对象也会自动被删除。 上述函数生成一个新的UI线程对象pBounceThread,并调用CreateThread函数创建线程。至此,线程已被创建,但还需要做初始化工作,如下面的函数InitInstance所示: int CBounceThread::InitInstance() { CWnd* pParent = CWnd::FromHandle(m_hwndParent); CRect rect; pParent->GetClientRect(&rect); BOOL bReturn = m_wndBounce.Create(_T("BounceMTChildWnd"),WS_CHILD | WS_VISIBLE, rect, pParent); if (bReturn) m_pMainWnd = &m_wndBounce; return bReturn; } 注意:这里,将m_pMainWnd设置为新创建的CBounceWnd窗口是必需的。只有这样设置了,才能保证当CBounceWnd窗口被删除时,线程会被自动删除。 (3)Mutexes 例程Mutexes是一个对话框程序。除主线程外,还有两个线程:一个用于计数,一个用于显示。在本例中,这两个线程都是从CWinThread派生出来的,但并不用于消息循环处理,派生类重载了Run函数,用于完成其计数和显示的任务。 在对话框类中使用了一个内嵌的CMutex对象。对话框初始化时创建两个线程,并设置相应的参数,然后启动运行两个线程。 当用户设置了对话框的同步检查框标记后,两个线程的同步处理有效。在计数线程的循环中,先调用CSingleLock::Lock函数,然后进行计数修改,最后调用CSingleLock::Unlock函数。注意:这里的CSingleLock对象根据主对话框的CMutex对象产生。在显示线程的循环中,先调用CSingleLock::Lock函数,然后取到计数值,最后调用CSingleLock::Unlock函数。注意:这里的CSingleLock对象也是由主对话框的CMutex对象产生。类似这种情况:一个线程要读取数据,另一个线程要修改数据,这是我们在处理多线程问题时碰到的最典型的情况。此处采用的方法也具有典型意义。源代码可通过查看例程或通过联机帮助来获取。 五、结束语 多线程函数是Win32不同于Win16的一个重要方面,其编程技术较为新颖,在程序设计思路上不同于传统的模块结构化方法,比一般的面向对象的思路也较为复杂,尤其是对于多处理器平台的处理更为复杂。要设计出性能良好的多线程程序,不仅需要对操作系统的处理过程很清楚,还需要对具体应用有一个全面的认识,并对应用中各线程部分的关系非常清楚,对同步模块中的同步对象的具体含义应尽可能地清晰明了,以利于在程序中控制同步事件的发生,避免出现死锁或不能同步处理的现象。 在其它的开发语言(如Visual Basic 5.0)中也提供了对多线程的支持,但从性能和安全的角度考虑,这种多线程支持受到较多的限制。不过,就一般应用而言,用这种处理方法已经足够了。 目前,大多数的计算机都是单处理器(CPU)的,在这种机器上运行多线程程序,有时反而会降低系统的性能。如果两个非常活跃的线程为了抢夺对CPU的控制权,则在线程切换时会消耗很多的CPU资源,但对于大部分时间被阻塞的线程(例如等待文件I/O操作),则可用一个单独的线程来完成。这样,就可将CPU时间让出来,使程序获得更好的性能。因此,在设计多线程应用程序时,应慎重选择,并且视具体情况加以处理,使应用程序获得最佳的性能。 |
线程
线程是一个能独立于程序的其他部分运行的作业。线程属于一个过程,获得自己的CPU时间片。基于WIN32的应用程序可以使用多个可执行的线程,称为多线程。Windows 3.x不能提供一种机制天然地支持多线程应用程序,但是一些为Windows 3.x编写应用程序的公司使用他们自己的线程安排。
基于WIN32的应用软件能在给定的过程中产生多个线程。依靠生成多个线程,应用程序能够完成一些后台操作,例如计算,这样程序就能运行得更快。当线程运行时,用户仍能继续影响程序。正如前面谈到的,当一个应用程序运行时,就产生了一个相应的过程。那么应用程序就能有一个单独的线程等待键盘输入或执行一个操作,例如脱机打印或计算电子表格中各项的总数。
在网络世界中,当你试图调整你站点的服务器的性能时,就要运行线程。如果你使用的是IIS,你可以在服务器上设置对于每个处理器所能创建的线程的最大数目。这样,就能在处理器间更均匀地分配工作,从而加速你的站点。
线程模式
现在,为了让你知道线程是什么和在哪能使用他们,让我们看一下使用线程时你可能要运行的应用程序:ActiveX组件。ActiveX组件是独立于其他代码运行,基于COM的代码。这听起来是不是很熟悉?当你使用ActiveX组件时,必须在操作系统中注册。其中的一条注册信息就是,这个ActiveX组件是否支持多个线程,如果支持怎样支持。这就是线程模式。
组件支持的基本线程模式有:单线程,单元线程,组合线程。下面几个部分将谈谈每一种模式对组件来说意味着什么。
单线程
如果组件被标记(即注册)为单线程组件,这就意味着所有可执行函数(称作方法)都将在组件的一个共享线程中运行。这就类似于没有生成独立的可执行线程的应用程序。单线程组件的缺点是一次只能运行一个方法。如果多次调用组件,例如调用组件中的存储方法,就会产生瓶颈,因为一次只能有一个调用。
如果你正在创建或使用一个ActiveX组件,建议不要使用单线程组件。
单元线程
如果一个组件被标记为单元线程,那么每个可执行的方法都将在一个和组件相联系的线程上运行。之所以成为单元线程是因为,每个新生成的组件实例都有一个相应的线程单元,每个正在运行的组件都有它自己的线程。单元线程组件要比单线程组件要好,因为多个组件可以在各自的单元中同时运行方法。
自由线程
一个自由线程组件是一个支持多线程单元的多线程组件。这意味着多个方法调用可同时运行,因为每个调用都有自己的运行线程。这能使你的组件运行快得多,但也有一些缺点。运行在同一单元中的单元组件可以在单元中直接调用其他组件的方法,这是一个非常快的操作。但是,自由线程组件必须从一个单元向另一个单元调用。为了实现这一操作,WIN32生成了一个代理,用来通过单元界线。这对于每个需要的功能调用来说就产生了系统开销,从而减低了系统的速度。每一个访问自由组件的调用都有一个相应的代理。既然代理调用比直接调用慢,那么自然会有性能方面的降低。
关于自由线程组件另一个需要注意的是:他们不是真正自由的。如果你创建了一个自由线程组件。你仍必须确保组件中的线程完全同步。这不是一件容易的事。只是简单地把你的组件标记为是自由线程的,并不能使你的组件支持多线程,你仍要去做使你的组件自由线程化的工作。如果你不做这个工作,你的共享数据可能被破坏。这里说明一下为什么:让我们假定你有一个方法计算某个数然后把它写到某个变量中。此方法被传入一个初始值例如是4,在随后的计算中这个变量的值增长为5。在方法结束时这个最后的值被写入到变量中。如果一次只有一个计算过程的话,所有这些会工作得很好。然而,当数据正在被改变时,另一个线程试图访问它,那么重新得到的数据就有可能是错误的。
组合线程
读到这,你也许会想既然每种形式的线程都有自己的优点和缺点,为什么不把不同的线程模式结合起来使用呢?组合线程模式也许符合你的要求。一个被标记为组合线程的组件既有单元线程组件的特性又有自由线程组件的特性。当一个组件被标记为组合线程时,这个组件将总是在和生成它的对象所在单元相同的单元中创建。如果组件是被一个标记为单线程的对象创建的,那么这个组件的行为将和一个单元线程组件一样,并且它将在线程单元中创建。这就意味着,组件和创建它的对象之间的调用,不需要一个为通信提供的代理调用。
如果新组件是被自由线程组件创建的,那么这个组件将表现得像一个自由线程组件,但是它将在同一单元中运行,因此新组件能够直接访问创建它的对象(既不需代理调用)。切记,如果你打算把你的组件标记为组合线程,你必须提供线程同步保护你的线程数据。
[前一篇]: 解剖恶意网站代码:开机后弹出IE页面
[后一篇]: VB的热键技巧的终结篇
-- 收录于:2001年10月18日 4:57
-- 作者: -- 最后出处:http://www.chinaasp.com
作者: |
评价: |
上站日期: 2001年09月05日 |
|
内容说明: |
|||
来源: |
|||
|
当我们需要在一个循环中传递参数时,使用使用函数的方法一般都是:
for(int I=0;I<100;I++){
fun(I); //使用函数传递i
}
每一个循环都会等待fun(I);函数执行完后再进行下一个循环。
但是当我们需要这个循环中创建线程,并将I的参数传递给线程时,如依然使用以上方法,会造成什么情况呢?
DWORD WINAPI ThreadFun(LPVOID lpParam){ //线程函数
Int *I = (int *)lpParam;
Return 0;
}
int I;
for(I=0;I<100;I++){
DWORD dwThreadId;
HANDLE hThread;
hThread = CreateThread(NULL,0,ThreadFunc,&I,0,&dwThreadId);
}
好了,到这里我们就可以发现,在循环中,我们创建线程并传递的参数是I=1后,主程序有可能在执到下一次循环时,第一次的ThreadFun函数仍未执行,而此时的I已经等2了,如果ThreadFun再来调用 Int *I = (int *)lpParam;语句时,显然不是我们想要的结果。
解决此问题的一种方法,便是可以使用静态数组来保存所要传递的参数。如下:
int I;
static int nPara[100]; //此句需定义为全局
for(I=0;I<100;I++){
DWORD dwThreadId;
HANDLE hThread;
NPara[I]=I; //保存参数
hThread = CreateThread(NULL,0,ThreadFunc,&nPara[I],0,&dwThreadId);
}
此时,所有参数均保存在nPara数组中,刚才的问题是解决了。
接下来又有了新的问题,让我们一起来看看吧:
1、如果需要创建的线程不止100,而是非常的大,而且我们也并不知道会有多少次循环的时侯。
2、如果我们需要传递的参数不单单只是一个int型的I,而是一个类。那么我们声明的时侯(假设线程数量最大为65535)则:
static CMYClass myClass[65535];
编译之后,得到的文件将会堆上一大堆的垃圾。相信任何一位程序都不想看到自己的程序上面堆了一堆垃圾在上面吧。
那么,还有没有更好的办法解决呢。答案是一定的,这里,我就讲一下我自己常用的方法:
动态创建对像传递参数。
一提到动态创建,我们自然会想到new 与 delete ,对了,我想说的也正是他们的使用。
假设参数类型为:
typedef struct _PARA{
int I;
DWORD dwNumber;
HWND hOther;
}Para;
使用new 在堆栈中申请一遍空间,在使用完后必需使用delete将其释放。
int I;
for(I=0;I<100;I++){
DWORD dwThreadId;
HANDLE hThread;
Para *myPara = new Para;
MyPara->I = I;
MyPara->dwNumber = 0 ;//自定
MyPara ->hOther = GetSafeHWnd();//当前窗体句柄
hThread = CreateThread(NULL,0,ThreadFunc,myPara,0,&dwThreadId);
}
//线程函数
DWORD WINAPI ThreadFun(LPVOID lpParam){ //线程函数
Para *myPara = (Para *)lpParam;
//执行其他功能
delete [] myPara; //释放
Return 0;
}
这样的话,也就不怕传递的参数多少与线程的数量太大了。另外如有需要的话可以加上一个线程计数器,保证当前线程的最大数量。
通常情况,我比较喜欢把线程处理放在一个类中处理,在主程序中尽量不与线程打交道。
以上是我在写ScanPort中遇到的一点点小问题,这里拿出来给大家一起分享,如果您有更好的方法,也请拿出与我们一起论讨。
AntGhazi/2001.12.21
mailto:[email protected]
http://antghazi.yeah.net
-------------------------------------------------------------------------------
我试着用自已的话来表述线程的概念,还有很短时间里编的一个小示例程序(不知恰当不?,也不知能说得清不..),见笑了.
线程其实和标准的windows主程序(WinMain)没啥两样...主程序其实是一个特殊的线程,称为主线程而已,其实你完全可以把线程想象成和winmain一起**同时运行**,但是** 可以相互访问(即在一个地址空间) **的一些小的WinMain程序.它和主线程一样,里面可以创建窗口,获取消息,等等..
由于线程们在一个地址空间且同时运行,所以会造成一些麻烦。因为我们编程都要用别人的函数库,而他们的函数库里面往往会有很多静态或全局的状态或中间变量,有着很复杂的相互依赖关系,如果执行某个功能不串行化(所谓串行化,也就是只能等一个功能调用返回后,另一个线程才能调用,不可以同时调用),就会造成大乱.这对线程来说,有术语称同步,windows为我们提供了很多同步的方法,MFC也提供了一些同步核心对象的类封装.对于某个功能调用库来说,叫线程安全.比如MFC的类库并不是线程安全的.
现在我举个刚刚自编的例子来简单说明这些概念。下面的一个对话框应用是多线程的.演示两个小动画:
(1)第一个动画由主线程的Timer来驱动,第二个动画由主线所创建的工作线程来驱动.分别显示在不同的位置.之所以我要加入Timer,也是为了形成线程驱动和timer驱动的对照,这是动画的两种驱动方式(还有在idle中驱动的)。
(2)这两个动画永远是不同的.也就是比如:一个是变哭,一个就会变笑,等那个变笑了,这个就变哭.动画图片来自于OICQ中的Face目录下,一般同样的头像会oicq会带三个图片(*-1.bmp,*-2.bmp,*-3.bmp),*-2.bmp是变灰的图片,我就取了1和3的图片来作动画.
这个程序的几个关键要注意的:
(1)主线程用PostThreadMessage和工作线程通信.工作线程用PeekMessage来取回消息。为了简单起见,我只用了一个WM_QUIT的消息来指示工作线程退出.
(2)主线程和工作线程同时调用了一个DisplayFace函数来进行动画显示.为了让两个动画一哭一笑做到不同,采用了CCriticalSection来进行同步.
示例如下:
(1)先用appwizards生成一个MFC的Dialog应用模板,假定对话框类为CTest01Dlg。
(2)再添入两个oicq的bmp文件到资源中去
(3)添加一个按钮(button)到对话框上.用作启动、停止动画的button
(4)用ClassWizard为button/onclick及dlg/ontimer生成事件响应函数,
(5)用Resource Symbol加入一个标识定义IDC_TIMER1
(6)在ClassView中为CTest01Dlg加入以下成员变量和成员函数
CriticalSection ccs;
CBitmap bm[2];
CWinThread* pMyThread;
static UINT MyThreadProc( LPVOID pParam);
void DisplayFace(CPoint r);
实现文件中加入相应代码(见下面)
(7)stdafx.h中加入#include
源代码如下,凡是我新加的代码周围都有注释包围,其它是ClassWizards自动写的:
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
file://
#if !defined(AFX_STDAFX_H__5B92DAA8_FE27_4702_8037_A2538343E69D__INCLUDED_)
#define AFX_STDAFX_H__5B92DAA8_FE27_4702_8037_A2538343E69D__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers
#include // MFC core and standard components
#include // MFC extensions
#include // MFC support for Internet Explorer 4 Common Controls
file://加入头引用主要是CCriticalSection对象的定义.
#include
file://加入结束
#ifndef _AFX_NO_AFXCMN_SUPPORT
#include // MFC support for Windows Common Controls
#endif // _AFX_NO_AFXCMN_SUPPORT
file://{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_STDAFX_H__5B92DAA8_FE27_4702_8037_A2538343E69D__INCLUDED_)
// test01Dlg.h : header file
file://
#if !defined(AFX_TEST01DLG_H__F3780E23_CCFC_468C_A262_50FFF1D991BC__INCLUDED_)
#define AFX_TEST01DLG_H__F3780E23_CCFC_468C_A262_50FFF1D991BC__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
/////////////////////////////////////////////////////////////////////////////
// CTest01Dlg dialog
class CTest01Dlg : public CDialog
{
// Construction
public:
file://加入
CBitmap bm[2];
CCriticalSection ccs;
CWinThread* pMyThread;
static UINT MyThreadProc( LPVOID pParam);
void DisplayFace(CPoint r);
CTest01Dlg(CWnd* pParent = NULL); // standard constructor
file://加入结束
// Dialog Data
file://{{AFX_DATA(CTest01Dlg)
enum { IDD = IDD_TEST01_DIALOG };
// NOTE: the ClassWizard will add data members here
file://}}AFX_DATA
// ClassWizard generated virtual function overrides
file://{{AFX_VIRTUAL(CTest01Dlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
file://}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
file://{{AFX_MSG(CTest01Dlg)
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnButton1();
afx_msg void OnTimer(UINT nIDEvent);
file://}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
file://{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(AFX_TEST01DLG_H__F3780E23_CCFC_468C_A262_50FFF1D991BC__INCLUDED_)
// test01Dlg.cpp : implementation file
file://
#include "stdafx.h"
#include "test01.h"
#include "test01Dlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
/////////////////////////////////////////////////////////////////////////////
// CAboutDlg dialog used for App About
class CAboutDlg : public CDialog
{
public:
CAboutDlg();
// Dialog Data
file://{{AFX_DATA(CAboutDlg)
enum { IDD = IDD_ABOUTBOX };
file://}}AFX_DATA
// ClassWizard generated virtual function overrides
file://{{AFX_VIRTUAL(CAboutDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
file://}}AFX_VIRTUAL
// Implementation
protected:
file://{{AFX_MSG(CAboutDlg)
file://}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialog(CAboutDlg::IDD)
{
file://{{AFX_DATA_INIT(CAboutDlg)
file://}}AFX_DATA_INIT
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
file://{{AFX_DATA_MAP(CAboutDlg)
file://}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialog)
file://{{AFX_MSG_MAP(CAboutDlg)
// No message handlers
file://}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CTest01Dlg dialog
CTest01Dlg::CTest01Dlg(CWnd* pParent /*=NULL*/)
: CDialog(CTest01Dlg::IDD, pParent)
{
file://{{AFX_DATA_INIT(CTest01Dlg)
// NOTE: the ClassWizard will add member initialization here
file://}}AFX_DATA_INIT
// Note that LoadIcon does not require a subsequent DestroyIcon in Win32
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
file://加入
pMyThread =NULL;
file://加入结束
}
void CTest01Dlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
file://{{AFX_DATA_MAP(CTest01Dlg)
// NOTE: the ClassWizard will add DDX and DDV calls here
file://}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(CTest01Dlg, CDialog)
file://{{AFX_MSG_MAP(CTest01Dlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON1, OnButton1)
ON_WM_TIMER()
file://}}AFX_MSG_MAP
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// CTest01Dlg message handlers
BOOL CTest01Dlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add "About..." menu item to system menu.
// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// Set the icon for this dialog. The framework does this automatically
// when the application''s main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
file://加入
bm[0].LoadBitmap (IDB_BITMAP1);
bm[1].LoadBitmap (IDB_BITMAP3);
file://加入结束
return TRUE; // return TRUE unless you set the focus to a control
}
void CTest01Dlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialog::OnSysCommand(nID, lParam);
}
}
void CTest01Dlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, (WPARAM) dc.GetSafeHdc(), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialog::OnPaint();
}
}
HCURSOR CTest01Dlg::OnQueryDragIcon()
{
return (HCURSOR) m_hIcon;
}
file://加入
void CTest01Dlg::OnButton1()
{
static BOOL bStarted=FALSE;
if (!bStarted){
SetTimer(IDC_TIMER1,500,NULL);
pMyThread=AfxBeginThread(MyThreadProc,this);
}else{
if (pMyThread){
pMyThread->PostThreadMessage (WM_QUIT,0,0);
::WaitForSingleObject(pMyThread->m_hThread ,INFINITE);
pMyThread=NULL;
}
KillTimer(IDC_TIMER1);
}
bStarted=!bStarted;
((CButton*)GetDlgItem(IDC_BUTTON1))->SetWindowText((bStarted?_T("停止"):_T("启动")));
}
void CTest01Dlg::OnTimer(UINT nIDEvent)
{
if (nIDEvent==IDC_TIMER1)
DisplayFace(CPoint(10,10));
CDialog::OnTimer(nIDEvent);
}
void CTest01Dlg::DisplayFace(CPoint p)
{
static int i=0;
ccs.Lock ();
BITMAP bmo;
bm[i].GetObject (sizeof(bmo),&bmo);
CClientDC dc(this);
CDC bmpDC;
bmpDC.CreateCompatibleDC (&dc);
bmpDC.SelectObject (&bm[i]);
dc.BitBlt (p.x ,p.y ,bmo.bmWidth,bmo.bmHeight,&bmpDC,0,0,SRCCOPY);
i++;
if (i==sizeof(bm)/sizeof(bm[0])) i=0;
ccs.Unlock ();
}
UINT CTest01Dlg::MyThreadProc(LPVOID pParam)
{
CTest01Dlg *me=(CTest01Dlg *)pParam;
MSG msg;
while(!PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
me->DisplayFace (CPoint(100,10));
::Sleep (200);
}
return 0;
}
file://加入结束
|
|
|
|
|
标题 |
VC++多线程下内存操作的优化 zdg(收藏) |
|
|
关键字 |
VC++多线程下内存操作的优化 |
|
|
作者/李红亚
许多程序员发现用VC++编写的程序在多处理器的电脑上运行会变得很慢,这种情况多是由于多个线程争用同一个资源引起的。对于用VC++编写的程序,问题出在VC++的内存管理的具体实现上。以下通过对这个问题的解释,提供一个简便的解决方法,使得这种程序在多处理器下避免出现运行瓶颈。这种方法在没有VC++程序的源代码时也能用。 问题 C和C++运行库提供了对于堆内存进行管理的函数:C提供的是malloc()和free()、C++提供的是new和delete。无论是通过malloc()还是new申请内存,这些函数都是在堆内存中寻找一个未用的块,并且块的大小要大于所申请的大小。如果没有足够大的未用的内存块,运行时间库就会向操作系统请求新的页。页是虚拟内存管理器进行操作的单位,在基于Intel的处理器的NT平台下,一般是4,096字节。当你调用free()或delete释放内存时,这些内存块就返还给堆,供以后申请内存时用。 这些操作看起来不太起眼,但是问题的关键。问题就发生在当多个线程几乎同申请内存时,这通常发生在多处理器的系统上。但即使在一个单处理器的系统上,如果线程在错误的时间被调度,也可能发生这个问题。 考虑处于同一进程中的两个线程,线程1在申请1,024字节的内存的同时,运行于另外一个处理器的线程2申请256字节内存。内存管理器发现一个未用的内存块用于线程1,同时同一个函数发现了同一块内存用于线程2。如果两个线程同时更新内部数据结构,记录所申请的内存及其大小,堆内存就会产生冲突。即使申请内存的函数者成功返回,两个线程都确信自己拥有那块内存,这个程序也会产生错误,这只是个时间问题。 产生这种情况称为争用,是编写多线程程序的最大问题。解决这个问题的关键是要用一个锁定机制来保护内存管理器的这些函数,锁定机制保证运行相同代码的多个线程互斥地进行,如果一个线程正运行受保护的代码,则其他的线程都必须等待,这种解决方法也称作序列化。 NT提供了一些锁定机制的实现方法。CreateMutex()创建一个系统范围的锁定对象,但这种方法的效率最低;InitializeCriticalSection()创建的critical section相对效率就要高许多;要得到更好的性能,可以用具有service pack 3的NT 4的spin lock,更详细的信息可以参考VC++帮助中的InitializeCriticalSectionAndSpinCount()函数的说明。有趣的是,虽然帮助文件中说spin lock用于NT的堆管理器(HeapAlloc()系列的函数),VC++运行库的堆管理函数并没有用spin lock来同步对堆的存取。如果查看VC++运行库的堆管理函数的源程序,会发现是用一个critical section用于全部的内存操作。如果可以在VC++运行库中用HeapAlloc(),而不是其自己的堆管理函数,将会因为使用的是spin lock而不是critical section而得到速度优化。 通过使用critical section同步对堆的存取,VC++运行库可以安全地让多个线程申请和释放内存。然而,由于内存的争用,这种方法会引起性能的下降。如果一个线程存取另外一个线程正在使用的堆时,前一个线程就需要等待,并丧失自己的时间片,切换到其他的线程。线程的切换在NT下是相当费时的,因为其占用线程的时间片的一个小的百分比。如果有多个线程同时要存取同一个堆,会引起更多的线程切换,足够引起极大的性能损失。
现象 如何发现多处理器系统存在这种性能损失?有一个简便的方法,打开“管理工具”中的“性能”监视器,在系统组中添加一个上下文切换/秒计数,然后运行想要测试的多线程程序,并且在进程组中添加该进程的处理器时间计数,这样就可以得到处理器在高负荷下要发生多少次上下文切换。 在高负荷下有上千次的上下文切换是正常的,但当计数超过80,000或100,000时,说明过多的时间都浪费在线程的切换,稍微计算一下就可以知道,如果每秒有100,000次线程切换,则每个线程只有10微秒用于运行,而NT上的正常的时间片长度约有12毫秒,是前者的上千倍。 图1的性能图显示了过度的线程切换,而图2显示了同一个进程在同样的环境下,在使用了下面提供的解决方法后的情况。图1的情况下,系统每秒钟要进行120,000次线程切换,改进后,每秒钟线程切换的次数减少到1,000次以下。两张图都是在运行同一个测试程序时截取得,程序中同时有3个线程同时进行最大为2,048字节的堆的申请,硬件平台是一个双Pentium II 450机器,有256MB内存。
解决方法 本方法要求多线程程序是用VC++编写的,并且是动态链接到C运行库的。要求NT系统所安装的VC++运行库文件msvcrt.dll的版本号是6,所安装的service pack的版本是5以上。如果程序是用VC++ v6.0以上版本编译的,即使多线程程序和libcmt.lib是静态链接,本方法也可以使用。 当一个VC++程序运行时,C运行库被初始化,其中一项工作是确定要使用的堆管理器,VC++ v6.0运行库既可以使用其自己内部的堆管理函数,也可以直接调用操作系统的堆管理函数(HeapAlloc()系列的函数),在__heap_select()函数内部分执行以下三个步骤: 1、检查操作系统的版本,如果运行于NT,并且主版本是5或更高(Window 2000及以后版本),就使用HeapAlloc()。 2、查找环境变量__MSVCRT_HEAP_SELECT,如果有,将确定使用哪个堆函数。如果其值是__GLOBAL_HEAP_SELECTED,则会改变所有程序的行为。如果是一个可执行文件的完整路径,还要调用GetModuleFileName()检查是否该程序存在,至于要选择哪个堆函数还要查看逗号后面的值,1表示使用HeapAlloc(),2表示使用VC++ v5的堆函数,3表示使用VC++ v6的堆函数。 3、检测可执行文件中的链接程序标志,如果是由VC++ v6或更高的版本创建的,就使用版本6的堆函数,否则使用版本5的堆函数。 那么如何提高程序的性能?如果是和msvcrt.dll动态链接的,保证这个dll是1999年2月以后,并且安装的service pack的版本是5或更高。如果是静态链接的,保证链接程序的版本号是6或更高,可以用quickview.exe程序检查这个版本号。要改变所要运行的程序的堆函数的选取,在命令行下键入以下命令: set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 以后,所有从这个命令行运行的程序,都会继承这个环境变量的设置。这样,在堆操作时都会使用HeapAlloc()。如果让所有的程序都使用这些速度更快的堆操作函数,运行控制面板的“系统”程序,选择“环境”,点取“系统变量”,输入变量名和值,然后按“应用”按钮关闭对话框,重新启动机器。 按照微软的说法,可能有一些用VC++ v6以前版本编译程序,使用VC++ v6的堆管理器会出现一些问题。如果在进行以上设置后遇到这样的问题,可以用一个批处理文件专门为这个程序把这个设置去掉,例如: set __MSVCRT_HEAP_SELECT=c:/program files/myapp/myapp.exe,1 c:/bin/buggyapp.exe,2
测试 为了验证在多处理器下的效果,编了一个测试程序heaptest.c。该程序接收三个参数,第一个参数表示线程数,第二个参数是所申请的内存的最大值,第三个参数每个线程申请内存的次数。 #define WIN32_LEAN_AND_MEAN #include #include #include #include /* compile with cl /MT heaptest.c */ /* to switch to the system heap issue the following command before starting heaptest from the same command line set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 */ /* structure transfers variables to the worker threads */ typedef struct tData { int maximumLength; int allocCount; } threadData; void printUsage(char** argv) { fprintf(stderr,"Wrong number of parameters./nUsage:/n"); fprintf(stderr,"%s threadCount maxAllocLength allocCount/n/n", argv[0]); exit(1); } unsigned __stdcall workerThread(void* myThreadData) { int count; threadData* myData; char* dummy; srand(GetTickCount()*GetCurrentThreadId()); myData=(threadData*)myThreadData; /* now let us do the real work */ for(count=0;count { dummy=(char*)malloc((rand()%myData->maximumLength)+1); free(dummy); } _endthreadex(0); /* to satisfy compiler */ return 0; } int main(int argc,char** argv) { int threadCount; int count; threadData actData; HANDLE* threadHandles; DWORD startTime; DWORD stopTime; DWORD retValue; unsigned dummy; /* check parameters */ if(argc<4 || argc>4) printUsage(argv); /* get parameters for this run */ threadCount=atoi(argv[1]); if(threadCount>64) threadCount=64; actData.maximumLength=atoi(argv[2])-1; actData.allocCount=atoi(argv[3]); threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE)); printf("Test run with %d simultaneous threads:/n",threadCount); startTime=GetTickCount(); for(count=0;count { threadHandles[count]=(HANDLE)_beginthreadex(0,0, &workerThread, (void*)&actData,0,&dummy); if(threadHandles[count]==(HANDLE)-1) { fprintf(stderr,"Error starting worker threads./n"); exit(2); } } /* wait until all threads are done */ retValue=WaitForMultipleObjects(threadCount,threadHandles ,1,INFINITE); stopTime=GetTickCount(); printf("Total time elapsed was: %d milliseconds", stopTime-startTime); printf(" for %d alloc operations./n", actData.allocCount*threadCount); /* cleanup */ for(count=0;count CloseHandle(threadHandles[count]); free(threadHandles); return 0; } 测试程序在处理完参数后,创建参数1指定数量的线程,threadData结构用于传递计数变量。workThread中进行内存操作,首先初始化随机数发生器,然后进行指定数量的malloc()和free()操作。主线程调用WaitForMultipleObject()等待工作者线程结束,然后输出线程运行的时间。计时不是十分精确,但影响不大。 为了编译这个程序,需要已经安装VC++ v6.0程序,打开一个命令行窗口,键入以下命令: cl /MT heaptest.c /MT表示同C运行库的多线程版静态链接。如果要动态链接,用/MD。如果VC++是v5.0的话并且有高版本的msvcrt.dll,应该用动态链接。现在运行这个程序,用性能监视器查看线程切换的次数,然后按上面设置环境参数,重新运行这个程序,再次查看线程切换次数。 当截取这两张图时,测试程序用了60,953ms进行了3,000,000次的内存申请操作,使用的是VC++ v6的堆操作函数。在转换使用HeapAlloc()后,同样的操作仅用了5,291ms。在这个特定的情况下,使用HeapAlloc()使得性能提高了10倍以上!在实际的程序同样可以看到这种性能的提升。
结论 多处理器系统可以自然提升程序的性能,但如果发生多个处理器争用同一个资源,则可能多处理器的系统的性能还不如单处理器系统。对于C/C++程序,问题通常发生在当多个线程进行频繁的内存操作活动时。如上文所述,只要进行很少的一些设置,就可能极大地提高多线程程序在多处理器下的性能。这种方法即不需要源程序,也不需要重新编译可执行文件,而最大的好处是用这种方法得到的性能的提高是不用支付任何费用的。
|
标题 |
任务,过程,和线程 ghj1976(转贴) |
|
|
关键字 |
任务,过程,和线程 |
|
|
出处 |
http://www.microsoft.com/china/msdn/techvoice/09.asp |
|
|
在你很小的时候,你学习怎样一次完成一个工作,但到了成年,当然也许你已成为父母辈了,这时,你就必须学会如何在同一时间做多个工作。例如,你是否发现有多少次你在办公室里作弄过电话,E-MAIL 和客人?你也许正在办公室里和一个人在谈话,这时电话铃响了,谈话就被电话打断了。也许正在打电话时,你又来了新(重要的)E-MAIL,你必须中断你的电话。一旦处理完E-MAIL以后,你可以接着打电话或者继续和办公室里的人谈话。如果等的时间太长了,电话另一端的人可能会挂断电话,或者办公室里的人也许会生气离开。你必须决定如何花费你的时间和花在谁身上。 在上面的例子中,你就是在进行多任务处理。你决定如何花费你的时间和计算机对多任务进行时序安排类似。正如我们一天只有24小时,计算机的资源也是有限的。看起来好像你在同时打电话和处理E-MAIL,但事实上,每次你只能将注意力集中在一件事情上。实际上你是在不断变换注意力,只不过变换得足够快,就好像你在同时做多个工作一样。当计算机处理多任务时,它从一个程序到另一个程序的切换是非常快的,以至于使你认为,所有 的程序都在同时运行。
图一,从用户观点看单任务和多任务 假如你有一个克隆(可能叫多莉)的话,你就能同时做两件事。对于有多个CPU的计算机,同时在每一个CPU上运行程序称为多重处理。有时,人们可替换使用多任务处理和多重处理两个概念,但是,你不能在没有多个处理器的计算机上进行多重处理。因此,如果你正在使用一台只有一个处理器的计算机,操作系统可以进行多任务处理,如果你正在使用一台有多个处理器的计算机,操作系统既能进行多任务处理又能进行多重处理。 在操作系统中,过程是一个逻辑上的任务。过程是在运行应用程序,启动某一系统服务,和在Windows NT中启动某一子系统时产生的。每一个过程都有自己的专用资源(例如自己的专用存储空间),只有拥有这个过程的应用程序才能访问这些资源。这意味着,如果你产生了一个程序,在程序中用到一些数据,并且你也没有建立任何形式的程序间数据共享机制或使用操作系统的程序间数据共享机制,那么就只有你的程序能访问这些数据。大多数Windows开发者都用COM对象简化程序间的通信。如果你用的是Windows NT并且想共享数据,你可以使用内存映像文件。但是这对于Geek Speak column来说技术性太强,所以让我们继续往下讲。 多任务处理:合作的和有优先权的 你能使用的多任务处理有两种基本方法:合作的,在这种方法中,正在运行的过程必须为其他过程留出CPU时间片;有优先权的,在这种方法中,操作系统决定哪个程序获得时间片。Microsoft Windows 3.x和Macintosh用的都是合作的多任务处理,而OS/2,Windows 95,Windows NT,UNIX,和Amiga操作系统使用的是有优先权的多任务处理。 合作的多任务处理 如果使用合作的多任务处理,每个程序必须允许其他程序使用CPU。使用合作的多任务处理系统的应用软件都有一个特殊的码环,这个码环产生控制允许其他应用软件的运行。如果每个人都按规则办事,这种方法会工作得相当好,但是当应用程序不服从这一规则时,他“霸占”CPU。这意味着,终端用户不能转向其他应用程序,使操作系统或应用程序出现“挂起”。 有优先权的多任务处理 在有优先权的多任务处理中,操作系统安排CPU时间,一个应用软件在任何时候都有可能被操作系统暂停(先占)。这减轻了“一玩到底”的程序问题,因为操作系统负责分给每个应用软件自己的时间片。 Windows 95对于32位Windows应用程序采用有优先权的多任务处理,为了能够向下兼容,对于16位的Windows应用程序(为Windows 3.x写的应用程序)仍采用合作的多任务处理。 线程 线程是一个能独立于程序的其他部分运行的作业。线程属于一个过程,获得自己的CPU时间片。基于WIN32的应用程序可以使用多个可执行的线程,称为多线程。Windows 3.x不能提供一种机制天然地支持多线程应用程序,但是一些为Windows 3.x编写应用程序的公司使用他们自己的线程安排。 基于WIN32的应用软件能在给定的过程中产生多个线程。依靠生成多个线程,应用程序能够完成一些后台操作,例如计算,这样程序就能运行得更快。当线程运行时,用户仍能继续影响程序。正如前面谈到的,当一个应用程序运行时,就产生了一个相应的过程。那么应用程序就能有一个单独的线程等待键盘输入或执行一个操作,例如脱机打印或计算电子表格中各项的总数。 在网络世界中,当你试图调整你站点的服务器的性能时,就要运行线程。如果你使用的是IIS,你可以在服务器上设置对于每个处理器所能创建的线程的最大数目。这样,就能在处理器间更均匀地分配工作,从而加速你的站点。 线程模式 现在,为了让你知道线程是什么和在哪能使用他们,让我们看一下使用线程时你可能要运行的应用程序:ActiveX组件。ActiveX组件是独立于其他代码运行,基于COM的代码。这听起来是不是很熟悉?当你使用ActiveX组件时,必须在操作系统中注册。其中的一条注册信息就是,这个ActiveX组件是否支持多个线程,如果支持怎样支持。这就是线程模式。 组件支持的基本线程模式有:单线程,单元线程,组合线程。下面几个部分将谈谈每一种模式对组件来说意味着什么。 单线程 如果组件被标记(即注册)为单线程组件,这就意味着所有可执行函数(称作方法)都将在组件的一个共享线程中运行。这就类似于没有生成独立的可执行线程的应用程序。单线程组件的缺点是一次只能运行一个方法。如果多次调用组件,例如调用组件中的存储方法,就会产生瓶颈,因为一次只能有一个调用。 如果你正在创建或使用一个ActiveX组件,建议不要使用单线程组件。 单元线程 如果一个组件被标记为单元线程,那么每个可执行的方法都将在一个和组件相联系的线程上运行。之所以成为单元线程是因为,每个新生成的组件实例都有一个相应的线程单元,每个正在运行的组件都有它自己的线程。单元线程组件要比单线程组件要好,因为多个组件可以在各自的单元中同时运行方法。 自由线程 一个自由线程组件是一个支持多线程单元的多线程组件。这意味着多个方法调用可同时运行,因为每个调用都有自己的运行线程。这能使你的组件运行快得多,但也有一些缺点。运行在同一单元中的单元组件可以在单元中直接调用其他组件的方法,这是一个非常快的操作。但是,自由线程组件必须从一个单元向另一个单元调用。为了实现这一操作,WIN32生成了一个代理,用来通过单元界线。这对于每个需要的功能调用来说就产生了系统开销,从而减低了系统的速度。每一个访问自由组件的调用都有一个相应的代理。既然代理调用比直接调用慢,那么自然会有性能方面的降低。 关于自由线程组件另一个需要注意的是:他们不是真正自由的。如果你创建了一个自由线程组件。你仍必须确保组件中的线程完全同步。这不是一件容易的事。只是简单地把你的组件标记为是自由线程的,并不能使你的组件支持多线程,你仍要去做使你的组件自由线程化的工作。如果你不做这个工作,你的共享数据可能被破坏。这里说明一下为什么:让我们假定你有一个方法计算某个数然后把它写到某个变量中。此方法被传入一个初始值例如是4,在随后的计算中这个变量的值增长为5。在方法结束时这个最后的值被写入到变量中。如果一次只有一个计算过程的话,所有这些会工作得很好。然而,当数据正在被改变时,另一个线程试图访问它,那么重新得到的数据就有可能是错误的。下面的图表说明了这一点。 为了修正这一错误,开发者为对象提供了线程同步。线程同步是在正在运行你想保护的某一其他代码时运行的代码。操作系统并不先占这个代码,直到获得一个可以中断的信号。如果你想了解更多的有关线程同步对象的详细内容,你不应该阅读Geek Speak column!不,我的意思是,“注意看一下本文后面列出的参考阅读文献”。
图二,共享数据被多线程访问搞乱了
组合线程 读到这,你也许会想既然每种形式的线程都有自己的优点和缺点,为什么不把不同的线程模式结合起来使用呢?组合线程模式也许符合你的要求。一个被标记为组合线程的组件既有单元线程组件的特性又有自由线程组件的特性。当一个组件被标记为组合线程时,这个组件将总是在和生成它的对象所在单元相同的单元中创建。如果组件是被一个标记为单线程的对象创建的,那么这个组件的行为将和一个单元线程组件一样,并且它将在线程单元中创建。这就意味着,组件和创建它的对象之间的调用,不需要一个为通信提供的代理调用。 如果新组件是被自由线程组件创建的,那么这个组件将表现得像一个自由线程组件,但是它将在同一单元中运行,因此新组件能够直接访问创建它的对象(既不需代理调用)。切记,如果你打算把你的组件标记为组合线程,你必须提供线程同步保护你的线程数据。 更多的信息 到此为止,你应该对过程,作业,线程有一个基本的了解了。如果你想进一步了解,下面这些文章也许对你有用。我必须事先提醒你,这些资料中的大多数并不是为初学者准备的。很好地理解COM,C++,和WIN32,将会对理解某些文章大有帮助。
|
by Jeffrey.Richter
vcbear 热情讲解
实现自己的同步对象?需要吗?
不需要吗?
...
只是跟你研究一下而已.
算了吧我只是个爱灌水的家伙,很久没有写代码了,闲来无事,灌灌水还不行吗?
1.概述:
在多进程的环境里,需要对线程进行同步.常用的同步对象有临界段(Critical Section),互斥量(Mutex),信号量(Semaphore),事件(Event)等,除了临界段,都是内核对象。
在同步技术中,临界段(Critical Section)是最容易掌握的,而且,和通过等待和释放内核态互斥对象实现同步的方式相比,临界段的速度明显胜出.但是临界段有一个缺陷,WIN32文档已经说明了临界段是不能跨进程的,就是说临界段不能用在多进程间的线程同步,只能用于单个进程内部的线程同步.
因为临界段只是一个很简单的数据结构体,在别的进程的进程空间里是无效的。就算是把它放到一个可以多进程共享的内存映象文件里,也还是无法工作.
有甚么方法可以跨进程的实现线程的高速同步吗?
2.原理和实现
2.1为什么临界段快? 是“真的”快吗?
确实,临界段要比其他的核心态同步对象要快,因为EnterCriticalSection和LeaveCriticalSection这两个函数从InterLockedXXX系列函数中得到不少好处(下面的代码演示了临界段是如何使用InterLockedXXX函数的)。InterLockedXXX系列函数完全运行于用户态空间,根本不需要从用户态到核心态
之间的切换。所以,进入和离开一个临界段一般只需要10个左右的CPU执行指令。而当调用WaitForSingleObject之流的函数时,因为使用了内核对象,线程被强制的在用户态和核心态之间变换。在x86处理器上,这种变换一般需要600个CPU指令。看到这里面的巨大差距了把。
话说回来,临界段是不是真正的“快”?实际上,临界段只在共享资源没有冲突的时候是快的。当一个线程试图进入正在被另外一个线程拥有的临界段,即发生竞争冲突时,临界段还是等价于一个event核心态对象,一样的需要耗时约600个CPU指令。事实上,因为这样的竞争情况相对一般的运行情况来说是很少的(除非人为),所以在大部分的时间里(没有竞争冲突的时候),临界段的使用根本不牵涉内核同步,所以是高速的,只需要10个CPU的指令。(bear说:明白了吧,纯属玩概率,Ms的小花招)
2.3进程边界怎么办?
“临界段等价于一个event核心态对象”是什么意思?
看看临界段结构的定义先
typedef struct _RTL_CRITICAL_SECTION {
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
DWORD SpinCount;
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;
#typedef RTL_CRITICAL_SECTION CRITICL_SECTION
在CRITICAL_SECTION 数据结构里,有一个Event内核对象的句柄(那个undocument的结构体成员LockSemaphore,包含的实际是一个event的句柄,而不是一个信号量semaphore)。正如我们所知,内核对象是系统全局的,但是该句柄是进程所有的,而不是系统全局的。所以,就算把一个临界段结构直接放到共享的内存映象里,临界段也无法起作用,因为LockSemaphore里句柄值只对一个进程有效,对于别的进程是没有意义的。在一般的进程同步中,进程要使用一个存在于别的进程里的Event 对象,必须调用OpenEvent或CreaetEvent函数来得到进程可以使用的句柄值。
CRITICAL_SECTION结构里其他的变量是临界段工作所依赖的元素,Ms也“警告”程序员不要自己改动该结构体里变量的值。是怎么实现的呢?看下一步.
2.4 COptex,优化的同步对象类
Jeffrey Richter曾经写过一个自己的临界段,现在,他把他的临界段改良了一下,把它封装成一个COptex类。成员函数TryEnter拥有NT4里介绍的函数TryEnterCriticalSection的功能,这个函数尝试进入临界段,如果失败立刻返回,不会挂起线程,并且支持Spin计数.这个功能在NT4/SP3中被InitializeCriticalSectionAndSpinCount 和SetCriticalSectionSpinCount实现。Spin计数在多处理器系统和高竞争冲突情况下是很有用的,在进入WaitForXXX核心态之前,临界段根据设定的Spin计数进行多次TryEnterCtriticalSection,然后才进行堵塞。想一下,TryEnterCriticalSection才使用10个左右的周期,如果在Spin计数消耗完之前,冲突消失,临界段对象是空闲的,那么再用10个CPU周期就可以在用户态进入临界段了,不用切换到核心态.
(bear说:为了避免这个"核心态",Ms自己也是费劲脑汁呀.看出来了吧,优化的原则:在需要的时候才进入核心态。否则,在用户态进行同步)
以下是COptex代码。原代码下载
Figure 2: COptex
Optex.h
/******************************************************************************
Module name: Optex.h
Written by: Jeffrey Richter
Purpose: Defines the COptex (optimized mutex) synchronization object
******************************************************************************/
#pragma once
///////////////////////////////////////////////////////////////////////////////
class COptex {
public:
COptex(LPCSTR pszName, DWORD dwSpinCount = 4000);
COptex(LPCWSTR pszName, DWORD dwSpinCount = 4000);
~COptex();
void SetSpinCount(DWORD dwSpinCount);
void Enter();
BOOL TryEnter();
void Leave();
private:
typedef struct {
DWORD m_dwSpinCount;
long m_lLockCount;
DWORD m_dwThreadId;
long m_lRecurseCount;
} SHAREDINFO, *PSHAREDINFO;
BOOL m_fUniprocessorHost;
HANDLE m_hevt;
HANDLE m_hfm;
PSHAREDINFO m_pSharedInfo;
private:
BOOL CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount);
};
///////////////////////////////////////////////////////////////////////////////
inline COptex::COptex(LPCSTR pszName, DWORD dwSpinCount) {
CommonConstructor((PVOID) pszName, FALSE, dwSpinCount);
}
///////////////////////////////////////////////////////////////////////////////
inline COptex::COptex(LPCWSTR pszName, DWORD dwSpinCount) {
CommonConstructor((PVOID) pszName, TRUE, dwSpinCount);
}
Optex.cpp
/******************************************************************************
Module name: Optex.cpp
Written by: Jeffrey Richter
Purpose: Implements the COptex (optimized mutex) synchronization object
******************************************************************************/
#include
#include "Optex.h"
///////////////////////////////////////////////////////////////////////////////
BOOL COptex::CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount)
{
m_hevt = m_hfm = NULL;
m_pSharedInfo = NULL;
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
m_fUniprocessorHost = (sinf.dwNumberOfProcessors == 1);
char szNameA[100];
if (fUnicode) { // Convert Unicode name to ANSI
wsprintfA(szNameA, "%S", pszName);
pszName = (PVOID) szNameA;
}
char sz[100];
wsprintfA(sz, "JMR_Optex_Event_%s", pszName);
m_hevt = CreateEventA(NULL, FALSE, FALSE, sz);
if (m_hevt != NULL) {
wsprintfA(sz, "JMR_Optex_MMF_%s", pszName);
m_hfm = CreateFileMappingA(NULL, NULL, PAGE_READWRITE, 0, sizeof(*m_pSharedInfo), sz);
if (m_hfm != NULL) {
m_pSharedInfo = (PSHAREDINFO) MapViewOfFile(m_hfm, FILE_MAP_WRITE,
0, 0, 0);
// Note: SHAREDINFO's m_lLockCount, m_dwThreadId, and m_lRecurseCount
// members need to be initialized to 0. Fortunately, a new pagefile
// MMF sets all of its data to 0 when created. This saves us from
// some thread synchronization work.
if (m_pSharedInfo != NULL)
SetSpinCount(dwSpinCount);
}
}
return((m_hevt != NULL) && (m_hfm != NULL) && (m_pSharedInfo != NULL));
}
///////////////////////////////////////////////////////////////////////////////
COptex::~COptex() {
#ifdef _DEBUG
if (m_pSharedInfo->m_dwThreadId != 0) DebugBreak();
#endif
UnmapViewOfFile(m_pSharedInfo);
CloseHandle(m_hfm);
CloseHandle(m_hevt);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::SetSpinCount(DWORD dwSpinCount) {
if (!m_fUniprocessorHost)
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwSpinCount, dwSpinCount);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::Enter() {
// Spin, trying to get the Optex
if (TryEnter()) return;
DWORD dwThreadId = GetCurrentThreadId(); // The calling thread's ID
if (InterlockedIncrement(&m_pSharedInfo->m_lLockCount) == 1) {
// Optex is unowned, let this thread own it once
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId, dwThreadId);
m_pSharedInfo->m_lRecurseCount = 1;
} else {
// Optex is owned by a thread
if (m_pSharedInfo->m_dwThreadId == dwThreadId) {
// Optex is owned by this thread, own it again
m_pSharedInfo->m_lRecurseCount++;
} else {
// Optex is owned by another thread
// Wait for the Owning thread to release the Optex
WaitForSingleObject(m_hevt, INFINITE);
// We got ownership of the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId,
dwThreadId); // We own it now
m_pSharedInfo->m_lRecurseCount = 1; // We own it once
}
}
}
///////////////////////////////////////////////////////////////////////////////
BOOL COptex::TryEnter() {
DWORD dwThreadId = GetCurrentThreadId(); // The calling thread's ID
// If the lock count is zero, the Optex is unowned and
// this thread can become the owner of it now.
BOOL fThisThreadOwnsTheOptex = FALSE;
DWORD dwSpinCount = m_pSharedInfo->m_dwSpinCount;
do {
fThisThreadOwnsTheOptex = (0 == (DWORD)
InterlockedCompareExchange((PVOID*) &m_pSharedInfo->m_lLockCount,
(PVOID) 1, (PVOID) 0));
if (fThisThreadOwnsTheOptex) {
// We now own the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId,
dwThreadId); // We own it
m_pSharedInfo->m_lRecurseCount = 1; // We own it once
} else {
// Some thread owns the Optex
if (m_pSharedInfo->m_dwThreadId == dwThreadId) {
// We already own the Optex
InterlockedIncrement(&m_pSharedInfo->m_lLockCount);
m_pSharedInfo->m_lRecurseCount++; // We own it again
fThisThreadOwnsTheOptex = TRUE; // Return that we own the Optex
}
}
} while (!fThisThreadOwnsTheOptex && (dwSpinCount-- > 0));
// Return whether or not this thread owns the Optex
return(fThisThreadOwnsTheOptex);
}
///////////////////////////////////////////////////////////////////////////////
void COptex::Leave() {
#ifdef _DEBUG
if (m_pSharedInfo->m_dwThreadId != GetCurrentThreadId())
DebugBreak();
#endif
if (--m_pSharedInfo->m_lRecurseCount > 0) {
// We still own the Optex
InterlockedDecrement(&m_pSharedInfo->m_lLockCount);
} else {
// We don't own the Optex
InterlockedExchange((PLONG) &m_pSharedInfo->m_dwThreadId, 0);
if (InterlockedDecrement(&m_pSharedInfo->m_lLockCount) > 0) {
// Other threads are waiting, wake one of them
SetEvent(m_hevt);
}
}
}
///////////////////////////////// End of File /////////////////////////////////
使用这个COptex是很简单的事情,只要构造用下面这两种构造函数一个C++类的实例即可.
构造函数
COptex(LPCSTR pszName, DWORD dwSpinCount = 4000);
COptex(LPCWSTR pszName, DWORD dwSpinCount = 4000);
他们都调用了
BOOL CommonConstructor(PVOID pszName, BOOL fUnicode, DWORD dwSpinCount);
构造一个COptex对象必须给它一个字符串型的名字,在突破进程边界的时候这是必须的,只有这个名字能提供共享访问.构造函数支持ANSI或Unicode的名字。
当另外一个进程使用相同的名字构造一个COptex对象,构造函数如何发现已经存在的COptex对象?在CommonConstructor代码中用CreateEvent尝试创建一个命名Event对象,如果这个名字的Event对象已经存在,那么,得到该对象的句柄,并且GetLastError可以得到ERROR_ALREADY_EXISTS.如果不存在则创建一个.如果创建失败,则得到的句柄为NULL.
同样的,可以得到一个共享的内存映象文件的句柄.
构造成功后,在需要同步时,根据情况简单的执行相应的进程间同步操作。构造函数的第二个参数用来指定Spin计数,默认是4000(这是操作系统序列化堆Heap的函数所使用的数量.操作系统在分配和释放内存的时候,要序列化进程的堆,这时也要用到临界段)
COptex类的其他函数和Win32函数是一一对应的.熟悉同步对象的程序员应该很容易理解.
COptex是如何工作的呢?实际上,一个COptex包含两个数据块(Data blocks):一个本地的,私有的;另一个是全局的,共享的.一个COptex对象构造之后,本地数据块包含了COptex的成员变量:m_hevt变量初始化为一个命名事件对象句柄;m_hfm变量初始化为一个内存映象文件对象句柄.既然这些句柄代表的对象是命名的,那么,他们可以在进程间共享。注意,是"对象"可以共享,而不是"对象的句柄".每个进程内的COptex对象都必须保持这些句柄在本进程内的值.
m_pShareInf成员指向一个内存映象文件,全局数据块在这个内存映象文件里,以指定的共享名存在. SHAREDINFO结构是内存映象数据的组织方式,该结构在COptex类里定义,和CRITCIAL_SECTION的结构非常相似.
typedef struct {
DWORD m_dwSpinCount;
long m_lLockCount;
DWORD m_dwThreadId;
long m_lRecurseCount;report-2001-03-07.htm
} SHAREDINFO, *PSHAREDINFO;
m_dwSpinCount : spin计数
m_lLockCount : 锁定计数
m_dwThreadID : 拥有该临界段的线程ID
m_lRecurseCount:本线程拥有该临界段的计数
好了,仔细看看代码吧,大师风范呀.注意一下在进行同步时,关于是否同一线程,关于LockCount的值的一系列的判断,以及InterLockedXXX系列函数的使用,具体用法查MSDN.
bear最喜欢这样的代码了,简单明了,思路清晰,原理超值,看完了只想大喝一声"又学一招,爽!"
bear也写累了 ,收工:).
2001.3.2
随意转载,只要不去掉Jeffrey的名字,还有bear的:D
翻译有错,请找[email protected]或留言,不懂Win32编程看下面:
Have a question about programming in Win32? Contact Jeffrey Richter at http://www.jeffreyrichter.com/
From the January 1998 issue of Microsoft Systems Journal.
原文网址:http://www.vchome.net/tech/multithread/thread23.htm#_Toc536438510