书名:C++多线程编程实战
ISBN:978-7-115-41366-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [黑山共和国] Miloš Ljumović
译 姜 佑
责任编辑 傅道坤
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright © Packt Publishing 2014. First published in the English language under the title C++ Multithreading Cookbook.
All Rights Reserved.
本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
本书是一本实践为主、通俗易懂的Windows多线程编程指导。本书使用C++本地调用,让读者能快速高效地进行并发编程。
全书共8章。第1章介绍了C++编程语言的概念和特性。
第2~5章介绍了进程、线程、同步、并发的相关知识。其中,第2章介绍进程和线程的基本概念,详细介绍了进程和线程对象。第3章讲解线程管理方面的知识,以及进程和线程背后的逻辑,简要介绍了线程同步、同步对象和同步技术。第4章重点介绍了消息传递技术、窗口处理器、消息队列和管道通信。第5章介绍了线程同步和并发操作,讲解了并行、优先级、分发器对象和调度技术,解释了同步对象(如互斥量、信号量、事件和临界区)。
第6章介绍.NET框架中的线程,概述了C++/CLI .NET线程对象。简要介绍了托管方法、.NET同步要素、.NET线程安全、基于事件的异步模式和BackgroundWorker对象,以及其他主题。
第7~8章为水平较高的读者准备了一些高级知识,概述了并发设计和高级线程管理。其中,第7章讲解理解并发代码设计,涵盖了诸如性能因素、正确性问题、活跃性问题的特性。第8章讲解高级线程管理,重点介绍更高级的线程管理知识。详细介绍了线程池的抽象、定制分发对象,以及死锁的解决方案。
附录涵盖了MySQL Connector C和WinDDK的具体安装步骤,介绍了如何为驱动程序编译和OpenMP编译设置Visual Studio。另外,还介绍了DebugView应用程序的安装步骤,并演示了它的使用步骤。
本书主要面向中高级读者,可作为用C++进行Windows多线程编程的参考读物。本书介绍的同步概念非常基础,因此也可作为对这方面技术感兴趣的读者和开发人员的参考书籍。
Miloš Ljumović于7月26日出生在欧洲黑山共和国的首都波德戈里察,在那里度过了小学和中学的时光,还到音乐学校学习了吉他。随后在黑山大学自然科学和数学学院进修了计算机科学。他对计算机浓厚兴趣,主修操作系统并获得了硕士学位。2009年12月,Miloš和他的朋友Danijel一起成立了自己的公司,作为一名程序员和高水平的团队一起致力于提供高技术含量的IT解决方案。不久,许多资深的开发者加入了他们,合作开发了许多应用程序和系统软件、web应用程序和数据库系统。他的客户不仅包括黑山政府,还涉及一些大型的国有企业,开发了一个新的金融系统MeNet以及一些与图片和其他数字媒体类型相关的视频识别软件。除此之外,他还开发了许多网站和其他网络应用程序。客户数量众多,不胜枚举。
Miloš作为国际顾问在美国一家大型的互联网电子商务贸易和数据采集公司工作了几个月。随后于2014年7月创立了一家新公司:EXPERT.ITS.ME。除了开发软件,他还为IT行业的小型企业提供咨询服务,鼓励并帮助他们在处理好企业管理问题的同时,把企业做大做强。另外,Miloš还是黑山国家委员会成员和门萨成员。他热爱编程,擅长C/C++/C#语言,精通HTML、PHP、TSQL等,梦想能开发出自己的操作系统。
在业余时间里,Miloš喜欢打网球、潜水、狩猎和下象棋。喜欢和自己的团队进行头脑风暴,想出一些在IT领域和计算机科学领域新鲜、时尚的好点子。他紧跟IT的发展步伐,不断学习新知识、解决新问题。尤其喜欢教授计算机科学和数学学生,在私有课堂和课程和分享它们给合格的程序员,帮助他们发现科学之美。想更多了解他的兴趣爱好和近况,请浏览他的公司网页(http://expert.its.me)或个人网页(http://milos.expert.its.me)。
本书特色
多线程编程是当今热门的一种编程技术。结合强大的C++,你可以轻松创建各种类型的应用程序、执行并行和优化现有程序。本书是一本实践为主、通俗易懂的C++ Windows多线程编程指导。你将学会如何从多线程方案中受益,提升自己的开发能力,构建更好的应用程序。本书不仅讲解了创建并行代码时会遇到的问题,而且还帮助读者更好地理解同步技术。本书最终的目标是帮助读者在理解多线程编程概念的同时,能快速有效地进行并行计算和并发编程。
本书包含以下内容:
撰写本书,感慨良多。
把本书献给我的父母Radosav和Slavka,以及我的姐姐Natalija和Dušanka。感谢家人无私地奉献和关爱。特别感谢我的母亲,没有她我不可能成为一名程序员。
非常感谢我美丽的妻子Lara的付出,她用耐心和爱鼓励着我,无条件地支持我,告诉我不要放弃。我爱你。
感谢我的好友Danijel,教我如何成为一个成功的商人,激励我每一天都能成为更好的程序员。
感谢黑山大学的老师们,没有他们我不可能成为现在这样的技术专家。特别感谢Rajko Ćalasan不厌其烦地教我编程,Milo Tomašević教我面向对象编程的专业知识,让我从此爱上C++。特别感谢我最好的老师Goran Šuković,他经常指导我,带我游历计算机科学的不同领域,让我每天都充满希望,以积极向上的态度学习新的知识。
Abhishek Gupta是印度班加罗尔的一位年轻的嵌入式软件工程师,开发自动车载信息娱乐系统软件多年。Abhishek于2011年在印度理工学院的卡哈拉格普尔理工学院,完成了视觉信息和嵌入系统专业技术硕士的学习。他对视频处理非常感兴趣,喜欢从事嵌入式多媒体系统工作,擅长用C和Linux进行编程。
欲详细了解他的信息,请访问www.abhitak.wordpress.com/about-me。
Venkateshwaran Loganathan是一位杰出的软件开发人员,他工作至今从事过设计、开发和软件产品的测试工作,有6年多的工作经验。Venkateshwaran早在11岁时就开始通过FoxPro学习计算机编程,从那以后,他学习并掌握了多种计算机语言,如C、C++、Perl、Python、Node.js和Unix shell脚本。他热衷于开源开发,为各种开源技术做出了贡献。
Venkateshwaran现就职于高知特科技公司(Cognizant Technology Solutions),作为一名技术助理从事物联网领域的研究和开发工作。目前,他活跃于使用射频识别(RFID)设备发展未来技术的概念。在加入高知特科技之前,他已经在一些大型IT公司工作多年,如Infosys、Virtusa和NuVeda。自从作为网络开发人员开始了他的职业生涯,他在网络、在线学习、医疗保健等各个专业领域都有涉猎。由于在工作上的突出表现,公司授予了他许多奖项和荣誉。
Venkateshwaran在安那大学获得计算机科学与工程的学士学位,目前正在攻读比尔拉技术与科学学院软件系统的硕士学位。除编程以外,他还钻研各种技术和软件技能,为新入职的工程师和在校学生授课。Venkateshwaran喜欢唱歌和徒步旅行,热衷参与社会服务,喜欢和人打交道。欲详细了解他的情况,请访问网站:http://www.venkateshwaranloganathan.com,并给他发邮件:anandvenkat4@gmail.com。
Venkateshwaran还写了一本书:《PySide GUI应用程序开发》(PySide GUI Application Development),已由Packt出版社出版。
我感慨良多。首先,感谢我的母亲Anbuselvi和祖母Saraswathi,感谢她们对我无私的付出,没有她们的支持和帮助,我不可能达到现在的水平。感谢我的兄弟和朋友们。在困难时期,他们一直都不离不弃地帮助我、祝福我。篇幅有限,无法一一列举所有帮助过我的人,我要在这里向他们表达最诚挚的感谢,感谢你们,我生命中的挚友们。
最重要的是,感谢全能的上帝,让我时刻沐浴在他无尽的祝福中。
Walt Stoneburner是一位经验丰富的软件架构师,有超过25年的商业应用程序开发和咨询经验,对质量保证、配置管理和安全都有所涉猎。如果刨根问底,他还会承认自己喜欢统计学和编写文档。
Walt对编程语言设计、协同应用程序、大数据、知识管理、数据可视化和ASCII艺术都很感兴趣。他说自己是壁橱极客。Walt还评估软件产品和消费性电子产品,画漫画,运营一家针对肖像和艺术的自由摄影工作室(CharismaticMoments.com),写一些幽默的段子,用手表演一些小戏法,喜爱游戏设计。此外,他还是一名业余无线电爱好者。
Walt有一个名为Walt-O-Matic的技术博客:http://www.wwco.com/~wls/blog/,通过wls@wwco.com或Walt.Stoneburner@ gmail.com可以直接与他取得联系。
他还参与了其他书籍的审稿:
Dinesh Subedi是Yomari私营有限责任公司的一位软件开发人员,目前从事数据仓库技术和商业智能开发。他毕业于尼泊尔加德满都工程学院(IOE)Pulchowk学校并获得了计算机工程学士学位。他在www.codeincodeblock.com写了四年的博客,发表了许多与C++软件开发相关的文章。
感谢我的兄弟Bharat Subedi在我审校本书时给予我的帮助。
多线程编程正逐渐成为IT行业和开发人员关注的焦点。开发商希望开发出用户友好、界面丰富,而且能并发执行的应用程序。强大的C++语言和本地Win32 API特性为多线程编程提供了良好开端。有了强大的C++,可以轻松地创建不同类型的应用程序,执行并行,而且还能优化现有的工作。
本书是一本实践为主、通俗易懂的Windows多线程编程指导。你将学到如何从多线程方案中受益,增强你的开发能力,构建更好的应用程序。本书不仅讲解了创建并行代码时遇到的问题,而且还帮助读者详细理解同步技术。此外,本书还涵盖了Windows进程模式、调度技术和进程间通信方面的内容。
本书从基础开始,介绍了最强大的集成开发环境:微软的Visual Studio。读者将学会使用Windows内核的本地特性和.NET框架的特性。除此之外,本书还详细讲解了如何解决某些常见的并发问题,让读者学会如何在多线程环境中正确地思考。
通过学习本书,读者将学会如何使用互斥量、信号量、临界区、监视器、事件和管道。本书介绍了C++应用程序中用到的大部分高效同步方式。本书用大量的程序示例,以最好的方式教会读者用C++开发并发应用程序。
本书使用C++本地调用,演示如何利用机器硬件来优化性能。本书最终的目标是传授各种多线程概念,让读者能快速高效地进行并行计算和并发编程。
第1章,C++概念和特性,介绍了C++编程语言和许多特性。本章重点介绍了程序的结构、执行流和Windows OS运行时对象。详细介绍了结构化方法和面向对象方法。
第2章,进程和线程的概念,详细介绍了进程和线程对象。本章涵盖了进程模式背后的思想和Windows进程的实现。除此之外,还介绍了进程间通信和典型的IPC问题,并简要介绍了在用户空间和内核中的线程实现。
第3章,管理线程,介绍了进程和线程背后的逻辑。本章涵盖了Windows OS特性,如协作式多任务和抢占式多任务。本章还详细介绍了线程同步以及同步对象和同步技术。
第4章,消息传递,重点介绍了消息传递技术、窗口处理器、消息队列和管道通信。
第5章,线程同步和并发操作,介绍了并行、优先级、分发器对象和调度技术。本章还解释了同步对象,如互斥量、信号量、事件和临界区。
第6章,.NET框架中的线程,概述了C++/CLI .NET线程对象。本章简要介绍了托管方法、.NET同步要素、.NET线程安全、基于事件的异步模式和BackgroundWorker对象,以及其他主题。
第7章,理解并发代码设计,涵盖了诸如性能因素、正确性问题、活跃性问题等特性。通过本章的学习,用户能从另一个更好的视角理解并发和并行应用程序设计。
第8章,高级线程管理,重点介绍更高级的线程管理知识。本章详细介绍了线程池的抽象、定制分发对象,以及死锁的解决方案。最后,本章介绍了一个远程线程的示例,演示高级管理。
附录涵盖了MySQL Connector C和WinDDK的具体安装步骤。另外,还介绍了如何为驱动程序编译和OpenMP编译设置Visual Studio。另外,还介绍了DebugView应用程序的安装步骤,演示了它的使用步骤。
要运行本书中的示例,必须安装下面的软件。
本书主要面向中级和高级水平的读者。本书介绍的同步概念非常基础,因此也可作为对这方面技术感兴趣的所有读者和开发人员的参考书籍。最后两章为水平较高的读者准备了一些高级知识,概述了并发设计和高级线程管理等主题。
本书通过不同的文本样式以区别不同类型的信息。这里介绍一下这些样式的示例,并解释其含义。
本书出现的代码、数据库表名、文件夹名称、文件名、文件扩展名、路径名、用户输入如下所示:“我们可以通过使用include
指令包含其他上下文”。
代码块如下所示:
{
public:
CLock(TCHAR* szMutexName);
~CLock();
private:
HANDLE hMutex;
};
inline CLock::CLock(TCHAR* szMutexName)
{
hMutex = CreateMutex(NULL, FALSE, szMutexName);
WaitForSingleObject(hMutex, INFINITE);
}
inline CLock::~CLock()
{
ReleaseMutex(hMutex);
CloseHandle(hMutex);
}
通过加粗的方式提醒读者注意代码块中的某些部分:
class CLock
{
public:
CLock(TCHAR* szMutexName);
~CLock();
private:
HANDLE hMutex;
};
inline CLock::CLock(TCHAR* szMutexName)
{
hMutex = CreateMutex(NULL, FALSE, szMutexName);
WaitForSingleObject(hMutex, INFINITE);
}
inline CLock::~CLock()
{
ReleaseMutex(hMutex);
CloseHandle(hMutex);
}
复制和粘贴本书中的代码时,要特别注意。由于书页篇幅有限,一些代码没法都放在一行,不得不分成多行。我们尝试解决这个问题,但是在某些情况下不太可能。我们强烈建议读者在开始编译示例之前,检查每一个示例,特别是那些带双引号的字符串。由于某些特殊情况,代码被分成多行显示,但要读者要明白这些代码是不能分行的,否则无法通过编译。
在“操作步骤”中,当文中写道“添加第1章的现有头文件CQueue.h
”时,我们的意思是:读者要使用Windows资源管理器,导航至存放有CQueue.h
文件的文件夹,并且把该文件以及与之相关的CList.h
复制到当前项目的工作文件夹中。然后在Visual Studio中通过【新增】-【现有项】,把该文件添加至项目中。这样才能正确地编译运行示例代码。
新术语和重要的文字加粗显示。菜单上的选项以这种形式显示:打开【解决方案资源管理器】,右键单击【头文件】。
警告或重要的事项。
本章介绍以下内容:
系统所执行的程序的进程或抽象是所有操作系统的核心概念。现在,绝大多数的操作系统在同一时间内都可以进行多项操作。例如,计算机在用户编辑Word文档时,还可以打印该文档、从硬盘缓冲区读数据、播放音乐等。在多任务操作系统中,中央处理单元(CPU)在程序中快速切换,执行每个程序只需几毫秒。
从严格意义上来说,对于单处理器系统,处理器在一个单元时间内只能执行一个进程。操作系统以极快的速度切换多个进程,营造了一个多进程同时运行的假象。与多处理器系统中硬件支持的真正并行相比,单处理器系统的这种并行叫伪并行(pseudoparallelism)更合适。
多线程(multithreading)是现代操作系统中非常重要的概念。多线程即允许执行多个线程,对完成并行任务和提升用户体验非常重要。
在传统的操作系统中,每个进程都有自己的地址空间和一个执行线程,该线程通常叫主线程(primary thread)。一般而言,运行在同一个进程中的多个线程具有相同的地址空间(即进程的地址空间),在准并行上下文中,这些线程就像是多个单独运行的进程,只不过它们的地址空间相同。
伪并行是操作系统在单处理器环境下的特性。准并行地址空间概念是Windows操作系统的特性。在多处理器系统中,Windows为每个进程提供了一个虚拟地址空间,比真正的物理地址空间大得多,因此叫做准并行上下文。
线程(thread)是操作系统中的一个重要概念。线程对象包含一个程序计数器(负责处理在下一次线程获取处理器时间时要执行什么指令)、一组寄存器(储存线程正在操控的变量当前值)、一个栈(储存与函数调用和参数相关的数据),等等。虽然线程执行在进程的上下文中,但是它们的区别很大。进程非常贪婪,想占用所有的资源;而线程比较“友好”,它们彼此合作、交流,而且共享资源(如处理器时间、内存和变量等)。
本书所有的程序示例均在Visal Studio IDE中运行。下面,针对Visal Studio介绍如何正确地设置IDE,并指出一些影响多线程应用程序的具体设置。
确定安装并运行了Visual Studio(VS)。
运行Visual Studio,在【开始】界面选择【新建项目】,会弹出一个有多个选项的窗口。在左边【模板】下面,选择【C++】,展开C++节点,有【CLR】、【常规】、【测试】、【Win32】等选项。然后,执行以下步骤。
1. 选择Win32。在中间栏有两个选项:【Win32控制台应用程序】和【Win32项目】。
目前,我们使用【Win32控制台应用程序】。【Win32项目】用于有图形用户接口(GUI)的应用程序,而不是控制台程序。如果使用控制台,要在项目属性中设置其他选项。
2. 选择【Win32控制台应用程序】,并在窗口下方的【名称】右边为项目命名。我们把第1个Win32控制台应用程序项目命名为TestProject
。在【位置】右边选择储存该项目文件的文件夹。VS将帮你创建一个文件夹,把用户刚才在【位置】输入的文件夹作为将来创建项目的默认文件夹。
现在,读者应该看到Win32应用程序向导窗口。可以直接单击右下方的【完成】,这样VS会自动创建所有需要的文件。或者,选择【下一步】,然后在附加选项中勾选【空项目】。如果这样做,就要自己创建源文件和头文件,VS不会自动生成所需的文件。
3. 如果在上一步骤的Win32应用程序向导窗口中直接选择【完成】,在【解决方案资源管理器】中就可以看到stdafx.h
和targetver.h
头文件,以及stdafx.cpp
和TestProject.cpp
源文件。stdafx.h
和stdafx.cpp
文件是预处理头文件的一部分,用于智能感应引擎。该引擎使用翻译单元(Translation Unit,TU)模型模仿命令行编译器,用于智能感应。典型的翻译单元由一个源文件和包含在源文件中的多个头文件组成。当然,其中还引用了其他头文件,所以也包含这些被引用的头文件。智能感应引擎从一个特殊的子串开始,给用户提供信息(如,特定类型是什么、函数和重载函数的原型是什么、在当前作用域中变量是否可用等)。欲了解更多相关内容,请查阅MSDN参考资料(http://msdn.microsoft.com )。
4. TestProject.cpp
文件出现在中间的窗口,这就是编写代码的地方。以后,我们会在更复杂的项目中创建和使用更多的文件,现在先暂时介绍这么多。
每个程序都必须有自己的主例程,即main
。当运行程序时,操作系统从调用main
开始。这是执行C++程序的起点。如果编写的代码遵循Unicode编程模型,就可以使用main
的宽字符版本wmain
。当然,也可以使用定义在TCHAR.h
中的_tmain
。如果定义了_UNICODE
,_tmain
函数相当于wmain
函数;如果没有定义_UNICODE
,_tmain
函数相当于main
函数。
在TestProject
窗口上方,有各种各样的按钮和选项。其中有一个包含Win32可选项的下拉菜单,这个选项叫做【解决方案平台】。如果要创建32位可执行文件,就不用改动。如果要创建64位可执行文件,先展开下拉菜单,选择【配置管理器】,找到【活动解决方案平台】,选择【x64】选项。点击【确定】,然后关闭【配置管理器】窗口。
在创建64位可执行文件时,最重要的是更改项目属性中的设置。按下Alt+F7,或者右键单击【解决方案资源管理器】中的TestProject
项目,选择【属性】,弹出TestProject
属性页窗口。在【配置属性】的【C/C++】的下拉菜单中选择【预处理器】。在【预处理器定义】中,把WIN32改成_WIN64才能创建64位可执行文件。其他设置暂不更改。
无论创建32位还是64位的代码,都要正确设置代码生成。创建C++项目时,可以选择该应用程序是否依赖用户PC上C++运行时所需的动态链接库(DLL)。如果创建的应用程序不仅在本机上运行,还要在其他PC上运行,就要考虑这一点。用VS在本机开发应用程序,所需的C++运行时库已经安装,不会有任何问题。但是,在其他未安装C++运行时库的PC上运行这种应用程序,就有可能出问题。如果确认不依赖DLL,则需把【运行时库】选项改为【多线程调试(/MTd)】的调试模式,或改为【多线程(/MT)】发布模式。调试模式或发布模式在【解决方案配置】的下拉菜单中可任意切换。
对于本书的程序示例,其他选项都不需要改动,因为32位和64位的机器都能运行32位可执行文件。运行时库作为C++软件包框架已经安装在PC中了,使用默认设置即可,应用程序在这样的PC中运行没有问题。
编程范式是计算机编程的基本样式,主要有4种范式:命令式、声明式、函数式(或结构式)、面向对象式。C++是当今最流行的面向对象编程语言之一,集功能性、灵活性、实用性于一体。和C一样,程序员能很快地适应它。C++成功的关键在于,程序员可以根据实际需要做相应地调整。
但是,C++学起来并不轻松。有时,你会认为这是一门高深莫测、难以捉摸的语言,一门永远学不完也无法完全理解和掌握的语言。别担心,学习一门语言并不是要掌握它的所有细枝末节,关键要学会如何正确地用语言特性解决特定的问题。实践是最好的老师,根据具体情况尽可能多地使用相应的特性,有助于加深理解。
在给出示例前,我们先介绍一下查尔斯·西蒙尼的匈牙利表示法。他在1977年的博士论文中,使用元编程(Meta-Programming)(一种软件生产方法)在程序设计中制定了标准的表示法。文中规定类型或变量的第1个字母表示数据类型。例如,如果要给一个类命名,Test
数据类型应该是CTest
。第1个字母C表示Test
是一个类。这个方法很不错,因为不熟悉Test
数据类型的程序员会马上明白Test
是一个类名。基本数据类型也可以这样处理,以int
和double
为例,iCount
表示一个int
类型的变量Count
,而dValues
表示一个double
类型的变量Value
。有了这些前缀,即使不熟悉代码也很容易识别它们的类型,提高了代码的可读性。
确定安装并运行了Visual Studio(VS)。
根据以下步骤创建我们的第1个程序示例。
1.创建一个默认的C++控制台应用程序[1],命名为TestDemo
。
2.打开TestDemo.cpp
。
3.输入下面的代码:
#include "stdafx.h"
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
cout << "Hello world" << endl;
return 0;
}
不同的编程技术使得C++程序的结构多种多样。绝大多数程序都必须有#include
或预处理指令。
#inlude <iostream>
告诉编译器包含iostream.h
头文件,该头文件中有许多函数原型。这也意味着函数实现以及相关的库都要放进可执行文件中。因此,如果要使用某个API或函数,就要包含相应的头文件,必要时还必须添加包含API或函数实现的输入库。另外,要注意<header>
和"header"
的区别。前者(< >
)表示从解决方案配置的项目路径开始搜索,而后者(""
)表示从与C++项目相关的当前文件夹开始搜索。
using
命令指示编译器要使用std
名称空间。名称空间中包含对象声明和函数实现,有非常重要的用途。在包含第三方库时,名称空间能最大程度地减少两个不同软件包中同名函数的歧义。
我们需要实现一个程序的入口点:main
函数。前面提到过, ANSI签名用main
, Unicode签名用wmain
,编译器根据项目属性页的预处理器定义确定签名用_tmain
。对于控制台应用程序,main
函数有以下4种不同的原型:
int _tmain(int argc, TCHAR* argv[])
void _tmain(int argc, TCHAR* argv[])
int _tmain(void)
void _tmain(void)
第1种原型有两个参数:argc
和argv
。第1个参数argc
(即,参数计数)表示第2个参数argv
(即,参数值)中的参数个数。形参argv
是一个字符串数组,其中的每个字符串都代表一个命令行参数。argv
中的第1个字符串一定是当前程序的名称。第2种原型和第1种原型的参数类型、参数个数相同,但是返回类型不同。这说明main
函数可能返回值,也可能不返回值。该值将被返回给操作系统。第3种原型没有参数,并返回一个整型值。第4种原型既没有参数也没有返回类型。看来,用第1种原型作为练习很不错。
函数体中的第1条语句使用了cout
对象。cout
是C++中标准输出流的名称。整条语句的意思是:把一系列字符(该例中是Hello world
字符序列)插入标准输出流(通常对应的是屏幕)。
cout
对象声明在std
名称空间的iostream
标准文件中。因此,要是用该对象必须包含相应的头文件,并且在_tmain
函数前面先声明其所属的名称空间。
在我们使用的原型中(int _tmain(int, _TCHAR*)
),_tmain
返回一个整数。因此,必须在return
关键字后面指定相应的int
类型值,本例中是0。向操作系统返回值时,0通常表示执行成功。但是,具体的值由操作系统决定。
这个小程序非常简单。我们以此为例解释main
例程作为每个C++程序入口点的基本结构和用法。
单线程程序按顺序逐行执行。因此,如果把所有的代码都写成一个线程,这样的程序对用户并不友好。
如图1.1所示,应用程序要等用户输入数据后,才能重新获得控制权继续执行。为此,可以创建并发线程来处理用户的输入。这样,应用程序随时都能响应,不会在等待用户输入时毫无反应了。线程处理完自己的任务后,可以给应用程序发信号,告诉程序用户已完成相应操作。
图1.1 单线程程序按顺序逐行执行
每次我们要在主执行流中单独执行一个操作,都必须考虑使用一个单独的线程。最简单的例子是,实现一边计算一边在进度条上反映计算的进度。想在同一个线程中处理计算和更新进度条,可能行不通。因为如果一个线程既要进行计算又要更新UI,就不能充分地与操作系统绘画交互。因此,一般情况下我们总是把UI线程与其他工作线程分开。
来看下面的例子。假设我们创建了一个用于计算的函数(如,计算指定角度的正弦值或余弦值),我们要同步显示计算过程的进度:
void CalculateSomething(int iCount)
{
int iCounter = 0;
while (iCounter++ < iCount)
{
// 计算部分
// 更新进度条部分
}
}
由于while
循环的每次迭代都忙于依次执行语句,操作系统没有所需的时间逐步更新用户接口(该例中,用户接口指进度条),因此用户见到的可能是空的进度条。待该函数返回后,才会出现已经完全被填满的进度条。出现这种情况的原因是在主线程中创建了进度条。我们应该单独用一个线程来执行CalculateSomething
函数,然后在每次迭代中给主线程发信号来逐步更新进度条。前面提到过,线程在CPU中以极快的速度切换,在我们看来进度条的更新与计算进度同步进行。
总而言之,每次处理并行任务时,如果要等待用户输入或依赖外部(如,远程服务器的响应),就应该为类似的操作单独创建一个线程,这样我们的程序才不会挂起无响应。
我们在后面的示例分析中会讨论静态库和动态库,现在先简要地介绍一下。静态库(*.lib
)通常是指一些已编译过的代码,放置在单独的文件中供将来使用。在项目中添加静态库就可以使用它的一些特性了。如前所示,代码中的#include <iostream>
指示编译器包含一个静态库,该库包含了一些输入输出流函数的实现。在实际运行程序之前,静态库在编译时被放入可执行文件。动态库(*.dll
)与静态库类似,不同的是它不在编译时放入,而在开始执行程序后才进行链接,也就是在运行时链接。如果许多程序都要用到某些函数,动态库就非常有用。有了动态库,就不必在每个程序中都包含这些函数,只需在运行程序时链接一个动态库即可。User32.dll
是一个很好的例子,Windows 操作系统把大部分GUI函数都放在该库中。因此,如果创建的两个程序都有窗口(GUI窗体),不必在两个程序中都包含CreateWindows
,只需在运行时链接User32.dll
,就可以使用CreateWindows
API了。
前面提到过,有4种编程范式。用C++编写程序时,可以使用结构化编程范式和面向对象编程范式。虽然C++是面向对象语言,但是也能用结构化编程方法来编写程序。通常,一个程序中有一个或多个函数,因为每个程序必须有一个主例程(main
)。对大型程序而言,如果把所有代码都放进main
函数中,会导致代码的可读性非常差。较好的做法是把程序中的代码分成多个处理单元,即函数。接下来,我们用一个计算两个复数之和的程序来说明。
确定安装并运行了Visual Studio。
执行下面的步骤。
1.创建一个新的默认C++控制台应用程序,命名为ComplexTest
。
2.打开ComplexTest.cpp
文件,并输入下面的代码:
#include "stdafx.h"
#include <iostream>
using namespace std;
void ComplexAdd(double dReal1, double dImg1, double dReal2,
double dImg2, double& dReal, double& dImg)
{
dReal = dReal1 + dReal2;
dImg = dImg1 + dImg2;
}
double Rand(double dMin, double dMax)
{
double dVal = (double)rand() / RAND_MAX;
return dMin + dVal * (dMax - dMin);
}
int _tmain(int argc, TCHAR* argv[])
{
double dReal1 = Rand(-10, 10);
double dImg1 = Rand(-10, 10);
double dReal2 = Rand(-10, 10);
double dImg2 = Rand(-10, 10);
double dReal = 0;
double dImg = 0;
ComplexAdd(dReal1, dImg1, dReal2, dImg2, dReal, dImg);
cout << dReal << "+" << dImg << "i" << endl;
return 0;
}
我们创建了ComplexAdd
函数,它有6个参数(或者3个复数)。前两个参数分别是第1个复数的实部和虚部;第3和第4个参数分别是第2个复数的实部和虚部;第5和第6个参数分别是第3个复数的实部和虚部,该复数是前两个复数之和。注意,第5和第6个参数按引用传递。另外,还创建了Rand
函数,返回一个dMin
和dMax
范围内的随机实数。
虽然上面的代码能完成任务,但是读者是否觉得其可读性很差?ComplexAdd
函数的参数太多,main
函数中又有6个变量。看来,这种处理方法并不“友好”。我们稍微改进一下,如下所示:
#include "stdafx.h"
#include <iostream>
using namespace std;
struct SComplex
{
double dReal;
double dImg;
};
SComplex ComplexAdd(SComplex c1, SComplex c2)
{
SComplex c;
c.dReal = c1.dReal + c2.dReal;
c.dImg = c1.dImg + c2.dImg;
return c;
}
double Rand(double dMin, double dMax)
{
double dVal = (double)rand() / RAND_MAX;
return dMin + dVal * (dMax - dMin);
}
int _tmain(int argc, TCHAR* argv[])
{
SComplex c1;
c1.dReal = Rand(-10, 10);
c1.dImg = Rand(-10, 10);
SComplex c2;
c2.dReal = Rand(-10, 10);
c2.dImg = Rand(-10, 10);
SComplex c = ComplexAdd(c1, c2);
cout << c.dReal << "+" << c.dImg << "i" << endl;
return 0;
}
这次,我们创建了一个新类型(在该例中是结构)SComplex
来表示复数,提高了代码的可读性,而且比之前的例子更有意义。因此,可以在代码中通过创建对象来执行抽象的任务,这样做提高了程序本身的逻辑性和可读性,以这种方式编程更容易。
面向对象编程(OOP)是为真实世界创建软件模型的全新方法,它以一种独特的方式设计程序。OOP有几个核心概念,如类、对象、继承和多态。
确定安装并运行了Visual Studio。
我们用OOP的方法修改一下前面的示例,请执行下面的步骤。
1.创建一个新的默认控制台应用程序,命名为ComplexTestOO
。
2.打开ComplexTestOO.cpp
文件,输入下面的代码:
#include "stdafx.h"
#include <iostream>
using namespace std;
double Rand(double dMin, double dMax)
{
double dVal = (double)rand() / RAND_MAX;
return dMin + dVal * (dMax - dMin);
}
class CComplex
{
public:
CComplex()
{
dReal = Rand(-10, 10);
dImg = Rand(-10, 10);
}
CComplex(double dReal, double dImg)
{
this->dReal = dReal;
this->dImg = dImg;
}
friend CComplex operator+ (const CComplex& c1, const CComplex& c2);
friend ostream& operator<< (ostream& os, const CComplex& c);
private:
double dReal;
double dImg;
};
CComplex operator+ (const CComplex& c1, const CComplex& c2)
{
CComplex c(c1.dReal + c2.dReal, c1.dImg + c2.dImg);
return c;
}
ostream& operator<< (ostream& os, const CComplex& c)
{
return os << c.dReal << "+" << c.dImg << "i";
}
int _tmain(int argc, TCHAR* argv[])
{
CComplex c1;
CComplex c2(-2.3, 0.9);
CComplex c = c1 + c2;
cout << c << endl;
return 0;
}
查看该例的main
函数会发现,这和做整数加法的main
函数差不多。复数并不是一种基本类型,程序员的工作就是添加一个类,并适当设置处理该类的方法。然后,就能像整数那样使用复数了。
在该例中,我们定义了一个新类型CComple
类。这个自定义的类有自己的特征[2]和方法,当然还可以有能访问其私有成员的友元方法。我们没有改变Rand
函数,只是让这个表示复数的类尽可能地像复数的抽象。我们创建了dReal
和dImg
特征分别表示复数的实部和虚部;创建了operator+
方法,让+
(加)对编译器而言有新的含义。改进后的代码可读性提高了,更容易理解,更方便使用。我们还创建了两个构造函数:一个是默认构造函数(使用-10~10的随机实数),一个是让用户直接设置复数实部和虚部的构造函数。
如果需要重载某个函数(稍后再详细介绍),有两种方案。第1种方案是在类中设置方法,该方法将改变主调对象的状态(或值)。第2种方案是把类和方法分开,即把方法放在类的外面,但是该方法必须声明为friend
才能访问对象中的私有和保护成员。
下面的代码告诉编译器,以两个复数为参数的operator+
函数被定义在别处,而且该函数要访问CComplex
类的私有和保护成员:
friend CComplex operator+(const CComplex& c1, const CComplex& c2);
这样做尽可能地简化了main
函数。把用户自定义类型当作基本类型来使用的好处是,即使原本不熟悉该程序的程序员,也能很快地理解代码所要表达的意思。
继承是OOP中非常重要的特性。继承至少关系到两个类(或更多类):如果B
类是某一种A
类,那么B
类的对象就拥有与A
类对象相同的属性。除此之外,B
类也可以实现新的方法和属性,以代替A
类相应的方法和属性。
确定安装并运行了Visual Studio。
现在,执行以下步骤来修改前面的示例。
1.创建一个默认控制台应用程序,命名为InheritanceTest
。
2.打开InheritanceTest.cpp
文件,输入下面的代码:
#include "stdafx.h"
#include <iostream>
using namespace std;
class CPerson
{
public:
CPerson(int iAge, char* sName)
{
this->iAge = iAge;
strcpy_s(this->sName, 32, sName);
}
virtual char* WhoAmI()
{
return "I am a person";
}
private:
int iAge;
char sName[32];
};
class CWorker : public CPerson
{
public:
CWorker(int iAge, char* sName, char* sEmploymentStatus)
: CPerson(iAge, sName)
{
strcpy_s(this->sEmploymentStatus, 32, sEmploymentStatus);
}
virtual char* WhoAmI()
{
return "I am a worker";
}
private:
char sEmploymentStatus[32];
};
class CStudent : public CPerson
{
public:
CStudent(int iAge, char* sName, char* sStudentIdentityCard)
: CPerson(iAge, sName)
{
strcpy_s(this->sStudentIdentityCard, 32, sStudentIdentityCard);
}
virtual char* WhoAmI()
{
return "I am a student";
}
private:
char sStudentIdentityCard[32];
};
int _tmain(int argc, TCHAR* argv[])
{
CPerson cPerson(10, "John");
cout << cPerson.WhoAmI() << endl;
CWorker cWorker(35, "Mary", "On wacation");
cout << cWorker.WhoAmI() << endl;
CStudent cStudent(22, "Sandra", "Phisician");
cout << cStudent.WhoAmI() << endl;
return 0;
}
我们创建了一个新的数据类型CPerson
来表示人。该类型用iAge
和sName
作为特征来描述一个人。如果还需要其他数据类型来表示工人或学生,就可以用OOP提供的一个很好的机制——继承来完成。工人首先是人,然后还有一些其他特征,我们用下面的代码把CPerson
扩展为CWorker
:
class CWorker : public CPerson
也就是说,CWorker
从CPerson
类继承而来。CWorker
类不仅具有基类CPerson
的所有特征和方法,还有一个对工人而言非常重要的特征sEmploymentStatus
。接下来,我们还要创建一个学生数据类型。除了年龄和名字,学生也还具有其他特征。同理,我们用下面的代码把CPerson
扩展为CStudent
:
class CStudent : public CPerson
声明一个对象时,要调用它的构造函数。这里要注意的是:声明一个派生类的对象时,先调用基类的构造函数,后调用派生类的构造函数。如下代码所示:
CWorker( int iAge, char* sName, char* sEmploymentStatus )
: CPerson( iAge, sName )
{
strcpy_s( this->sEmploymentStatus, 32, sEmploymentStatus );
}
注意看CWorker
构造函数的原型,其形参列表后面有一个:
(冒号),后面调用的是基类的构造函数,如下代码所示。在创建CPerson
时,需要两个参数iAge
和sName
:
CPerson(iAge, sName)
调用析构函数的顺序要反过来,即先调用派生类的析构函数,后调用基类的析构函数。
一图胜千言,CPerson
、CWorker
和CStudent
类对象分别如图1.2所示。
图1.2 CPerson、CWorker和CStudent类对象
可以针对用户自定义的类型来定义运算符的含义,如前面例子中的CComplex
。这样做非常好,当c
、c1
和c2
是复数时,c = c1 + c2
比c = ComplexAdd(c1, c2)
更直观更容易理解。
要让编译器能处理用户自定义的类型,就必须实现运算符函数或重载相应的函数。假设,有两个矩阵m1
、m2
和一个矩阵表达式m = m1 + m2
。编译器知道如何处理基本类型(如,把两个整数相加),但如果事先没有定义CMatrix operator+(const CMatrix& m1, const CMtrix& m2)
函数,编译器就不知道如何计算矩阵加法。
覆盖(override)方法也是一种特性,允许派生类在基类已经实现某方法的前提下提供自己的特定实现。如前面例子中的WhoAmI
方法所示,其输出如下:
I am a person I am a worker I am a student
虽然每个类中的方法名相同,但它们却是不同的方法,有不同的功能。我们可以说,CPerson
的派生类覆盖了WhoAmI
方法。
覆盖是OPP和C++的优异特性,不过多态更胜一筹。我们继续往下看。
利用多态(Polymorphism)特性,可以通过基类的指针或引用访问派生类的对象,执行派生类中实现的操作。
确定安装并运行了Visual Studio。
执行下面的步骤。
1. 创建一个新的默认控制台应用程序,名为PolymorphismTest
。
2. 打开PolymorphismTest.cpp
文件,并输入下面的代码:
#include "stdafx.h"
#include <iostream>
#define M_PI 3.14159265358979323846
using namespace std;
class CFigure
{
public:
virtual char* FigureType() = 0;
virtual double Circumference() = 0;
virtual double Area() = 0;
virtual ~CFigure(){ }
};
class CTriangle : public CFigure
{
public:
CTriangle()
{
a = b = c = 0;
}
CTriangle(double a, double b, double c) : a(a), b(b), c(c){ }
virtual char* FigureType()
{
return "Triangle";
}
virtual double Circumference()
{
return a + b + c;
}
virtual double Area()
{
double S = Circumference() / 2;
return sqrt(S * (S - a) * (S - b) * (S - c));
}
private:
double a, b, c;
};
class CSquare : public CFigure
{
public:
CSquare()
{
a = b = 0;
}
CSquare(double a, double b) : a(a), b(b) { }
virtual char* FigureType()
{
return "Square";
}
virtual double Circumference()
{
return 2 * a + 2 * b;
}
virtual double Area()
{
return a * b;
}
private:
double a, b;
};
class CCircle : public CFigure
{
public:
CCircle()
{
r = 0;
}
CCircle(double r) : r(r) { }
virtual char* FigureType()
{
return "Circle";
}
virtual double Circumference()
{
return 2 * r * M_PI;
}
virtual double Area()
{
return r * r * M_PI;
}
private:
double r;
};
int _tmain(int argc, _TCHAR* argv[])
{
CFigure* figures[3];
figures[0] = new CTriangle(2.1, 3.2, 4.3);
figures[1] = new CSquare(5.4, 6.5);
figures[2] = new CCircle(8.8);
for (int i = 0; i < 3; i++)
{
cout << "Figure type:\t" << figures[i]->FigureType()
<< "\nCircumference:\t" << figures[i]->Circumference()
<< "\nArea:\t\t" << figures[i]->Area()
<< endl << endl;
}
return 0;
}
首先,创建了一个新类型CFigure
。我们想创建一些具体的图形(如,三角形、正方形或者圆),以及计算这些图形周长和面积的方法。但是,我们并不知道具体的图形是什么类型,所以无法用方法直接计算图形的这些特性。这就是要把CFigure
类创建为抽象类的原因。抽象类是至少声明了一个虚方法的类,该虚方法没有实现,且其原型后面有= 0
。以这种方式声明的函数叫做纯虚函数。抽象类不能有对象,但是可以有继承类。因此可以实例化抽象类的指针和引用,然后从CFigure
类派生出CTriangle
、CSquare
和CCircle
类,分别表示三角形、正方形和圆形。我们要实例化这些对象的类型,所以在这些派生类中,实现了FigureType
方法、Circumference
方法和Area
方法。
虽然这3个类中的方法名都相同,但是它们的实现不同,这与覆盖类似但含义不同。如何理解?在本例的main
函数中,声明了一个数组,内含3个CFigure
类型的指针。作为指向基类的指针或引用,它们一定可以指向该基类的任何派生类。因此,可以创建一个CTriangle
类型的对象,并设置CFigure
类型的指针指向它,如下代码所示:
figures[ 0 ] = new CTriangle( 2.1, 3.2, 4.3 );
同理,用下面的代码可以设置其他图形:
figures[ 1 ] = new CSquare( 5.4, 6.5 );
figures[ 2 ] = new CCircle( 8.8 );
现在,考虑下面的代码:
for ( int i = 0; i < 3; i++ )
{
cout << "Figure type:\t" << figures[ i ]->FigureType( ) << "\nCircumference:\t"
<< figures[ i ]->Circumference( ) << "\nArea:\t\t" << figures[ i ]->Area( )
<< endl<<endl;
}
编译器将使用C++的动态绑定(dynamic binding)特性,确定图形指针具体指向哪个类型的对象,调用合适的虚方法。只有把方法声明为虚方法,且通过指针或引用访问才能使用动态绑定。
现在回头看看1.6节的例子。我们分别声明了CPerson
、CWorker
和CStudent
类型的对象,这是3个不同的类型。我们可以通过某个对象调用WhoAmI
方法,如下代码所示:
cPerson.WhoAmI()
编译器在编译时就知道cPerson
对象是CPerson
类型,也知道WhoAmI()
是该类型的方法。然而在本节的图形例子中,编译器在编译时并不知道图形指针将要指向哪个类型的对象,要等到运行时才知道。因此,这个过程叫动态绑定。
许多程序都要响应一些事件,例如,当用户按下按键或输入一些文本时。事件处理或程序能响应用户的动作是一种非常重要的机制。如果要在用户按下按键时处理这个事件,就要创建某种监听器,监听按键事件(即,按下的动作)。
事件处理器是操作系统调用的一个函数,每次都发送某种类型的消息。例如,在按下按键时发送“已按下”,在文本输入时发送“接收到一个字符”。
事件处理器非常重要。计时器是经过某段时间后触发的事件。当用户按下键盘上的一个按键,操作系统就引发“按下按键”事件,等等。
对我们而言,窗口的事件处理器至关重要。大多数应用程序都有窗口或窗体。每个窗口都要有自己的事件处理器,一旦在窗口中发生事件都要调用事件处理器。例如,如果创建一个带多个按钮和文本框的窗口,则必须有一个与该窗口相关的窗口过程来处理这些事件。
Windows操作系统以窗口过程的形式提供了这样一种机制,通常命名为WndProc
(也可以叫其他名称)。每次指定窗口发生事件时,操作系统就会调用该过程。在下面的例子中,我们将创建第1个Windows应用程序(即创建一个窗口),并解释窗口过程的用法。
确定安装并运行了Visual Studio。
执行下面的步骤。
1.创建一个新的C++ Win32项目,命名为GUIProject
,单击右下方的【确定】。在弹出的向导窗口中单击【下一步】,在附加选项中勾选【空项目】,然后单击【完成】。现在,在【解决方案资源管理器】中右键单击【源文件】,选择【添加】,然后左键单击【新建项】。在弹出的窗口中选择【C++
文件(.cpp)
】,命名为main
。然后,单击窗口右下方的【添加】。
2. 现在创建代码。首先,添加所需的头文件:#include <windows.h>
大多数API都需要windows.h
头文件才能处理一些视觉特性,如窗口、控件、枚举和样式。在创建一个应用程序入口点之前,必须先声明一个窗口过程的原型才能在窗口结构中使用它,如下代码所示:
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
我们稍后实现WndProc
,现在有声明就够了。接下来,需要一个应用程序入口点。Win32应用程序和控制台应用程序的main
函数原型稍有不同,如下代码所示:
int WINAPI WinMain(HINSTANCE hThis, HINSTANCE hPrev, LPSTR szCmdLine, int iCmdShow)
注意,在返回类型(int
)后面有一个WINAPI
宏,它表示一种调用约定(calling convention)。
WINAPI
或stdcall
意味着栈的清理工作由被调函数来完成。WinMain
是函数名,该函数必须有4个参数,而且参数的顺序要与声明中的顺序相同。第1个参数hThis
是应用程序当前实例的句柄。第2个参数hPrev
是应用程序上一个实例的句柄。如果查阅MSDN文档(http://msdn.microsoft.com/en-us/library/windows/desktop/ms633559%28v=vs.85%29.aspx)可以看到,hPrev
参数一定是NULL
。我猜应该是为了兼容旧版本的Windows操作系统,所以没有写明当前版本的值。第3个参数是szCmdLine
或应用程序的命令行,包括该程序的名称。最后一个参数控制如何显示窗口。
可以用OR
(|
)运算符组合多个位值(欲了解详细内容,请参阅MSDN)。
接下来,在WinMain
的函数体中,用UNREFERENCED_RARAMETER
宏告诉编译器不使用某些参数,方便编译器进行一些额外的优化。如下代码所示:
UNREFERENCED_PARAMETER( hPrev );
UNREFERENCED_PARAMETER( szCmdLine );
然后,实例化WNDCLASSEX
窗口结构。该对象中储存了待生成窗口的细节,如栈大小、当前应用程序实例的句柄、窗口样式、窗口颜色、图标和鼠标指针。WNDCLASSEX
窗口结构的实例化代码如下所示:
WNDCLASSEX wndEx = { 0 };
下面的代码定义了在实例化窗口类后分配的额外字节数:
wndEx.cbClsExtra = 0;
下面的代码定义了窗口结构的大小(以字节为单位):
wndEx.cbSize = sizeof( wndEx );
下面的代码定义了实例化窗口实例后分配的额外字节数:
wndEx.cbWndExtra = 0;
下面的代码定义了窗口类背景画刷的句柄:
wndEx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
下面的代码定义了窗口类光标的句柄:
wndEx.hCursor = LoadCursor( NULL, IDC_ARROW );
下面的代码定义了窗口类图标的句柄:
wndEx.hIcon = LoadIcon( NULL, IDI_APPLICATION );
wndEx.hIconSm = LoadIcon( NULL, IDI_APPLICATION );
下面的代码定义了包含窗口过程的实例句柄:
wndEx.hInstance = hThis;
下面的代码定义了指向窗口过程的指针:
wndEx.lpfnWndProc = WndProc;
下面的代码定义了指向以空字符结尾的字符串或原子的指针:
wndEx.lpszClassName = TEXT("GUIProject");
下面的代码定义了指向以空字符结尾的字符串的指针,该字符串指定了窗口类菜单的资源名:
wndEx.lpszMenuName = NULL;
下面的代码定义了窗口类的样式:
wndEx.style = CS_HREDRAW | CS_VREDRAW;
下面的代码注册一个窗口类,供CreateWindow
或CreateWindowEx
函数稍后使用:
if ( !RegisterClassEx( &wndEx ) )
{
return -1;
}
CreateWindow
API创建一个重叠、弹出的窗口或子窗口。它指定该窗口类、窗口标题、窗口样式、窗口的初始位置和大小(可选的)。该函数还指定了窗口的父窗口或所有者(如果有的话),以及窗口的菜单。如下代码所示:
HWND hWnd = CreateWindow( wndEx.lpszClassName, TEXT("GUI Project"), WS_OVERLAPPEDWINDOW,
200, 200, 400, 300, HWND_DESKTOP,NULL, hThis, 0 );
if ( !hWnd )
{
return -1;
}
如果指定窗口的更新域未被填满,UpdateWindow
函数就向窗口发送一条WM_PAINT
消息,更新指定窗口的客户区。该函数绕过应用程序的消息队列,向指定窗口的窗口过程直接发送一条WM_PAINT
消息。如下代码所示:
UpdateWindow( hWnd );
下面的代码设置指定窗口的显示状态:
ShowWindow( hWnd, iCmdShow );
我们还需要一个MSG
结构的实例来表示窗口消息。
MSG msg = { 0 };
接下来,进入一个消息循环。Windows中的应用程序是事件驱动的,它们不会显式调用函数(如,C运行时库调用)来获得输入,而是等待系统把输入传递给它们。系统把所有的输入传递给应用程序的不同窗口。每个窗口都有一个叫做窗口过程的函数,当有输入需要传递给窗口时,系统调用会调用该函数。窗口过程处理输入,并把控制权返回系统。GetMessage
API从主调线程的消息队列中检索信息,如下代码所示:
while ( GetMessage( &msg, NULL, NULL, NULL ) )
{
// 把虚拟键消息翻译成字符消息
TranslateMessage(&msg );
// 分发一条消息给窗口过程
DispatchMessage(&msg );
}
当关闭应用程序或发送一些触发其退出的命令时,系统会释放应用程序消息队列。这意味着该应用程序不会再有消息,而且while
循环也将结束。DestroyWindow
API销毁指定的窗口。该函数向指定窗口发送WM_DESTROY
和WM_NCDESTROY
消息,使窗口无效并移除其键盘焦点(keyboard focus)。此外,该函数还将销毁指定窗口的菜单,清空线程的消息队列,销毁与窗口过程相关的计时器,解除窗口对剪切板的所有权,如果该窗口在查看器链的顶端,还将打断剪切板的查看器链。
DestroyWindow( hWnd );
下面的函数注销窗口类,释放该类占用的内存:
UnregisterClass( wndEx.lpszClassName, hThis );
下面的return
函数从应用程序消息队列中返回一个成功退出代码或最后一个消息代码,如下代码所示:
return (int) msg.wParam;
以上,我们逐行讲解了WinMain
函数。接下来,要实现窗口过程或应用程序主事件处理器。作为第1个实例,先创建一个简单的WndProc
,它只有一个处理关闭窗口的功能。该窗口过程返回64位有符号长整型值,有4个参数:hWnd
结构(表示窗口标识符)、uMsg
无符号整数(表示窗口消息代码)、wParam
无符号64位长整型数(传递应用程序定义的数据)、lParam
有符号64位长整型数(也用于传递应用程序定义的数据)。
LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
消息代码负责处理消息,如默认消息(该例中是WM_CLOSE
),即正在关闭应用程序时系统发送的消息。然后,调用PostQuitMessage
API释放系统资源,并安全关闭该应用程序。
switch ( uMsg )
{
case WM_CLOSE:
{
PostQuitMessage( 0 );
break;
}
default:
{
最后,调用默认窗口过程(DefWindowProc
)处理应用程序未处理的窗口消息。该函数确保每个消息都被处理,如下所示:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return 0;
}
虽然本节介绍的窗口应用程序示例非常简单,但是它完整地反映了事件驱动系统特性和事件处理机制。在后面的章节中,我们将频繁地使用事件处理,所以理解这些基本过程非常重要。
下面的示例将演示线性链表(可包含任何泛型类型T
)的OOP用法。该示例背后的思想是把继承作为表示“B
是一种A
”这种关系的模型。
线性链表是一种线性排列元素的结构,第1个元素链接第2个元素,第2个元素链接第3个元素,以此类推。线性链表的基本操作是,在线性链表中插入元素(PUT
)和获取元素(GET
)。队列是一种线性链表,其中的元素按先进先出的次序排列,即FIFO(First In First Out)。因此,从队列的顶部获取元素,从队列的底部插入新元素。栈也是一种线性链表,从栈中获取元素的顺序与放入元素的顺序相反,也就是说,栈中的元素按先进后出的次序排列,即LIFO(Last In First Out)。
线性链表是按顺序排列的元素集合。每个链表都有用于设置元素值、从链表获取元素或简单查看元素的方法。链表可储存任何类型的对象。但是,为了满足链表类、队列和栈的特殊化,线性链表定义了插入元素和获取元素的精确位置。因此,作为泛化对象的链表是一个基类。
读者应该意识到,以这种方式设计的链表可实现为静态结构或动态结构。也就是说,这种链表可以实现为某种数组或结构,其中的各元素通过指针与相邻的元素链接。用指针来实现,可操作性强。
下面的示例中,将把线性链表实现为指针集合,这些指针指向用链表中的方法放置在链表中的原始对象。这样设计是为了实现链表元素的多态性,而不是把原始对象拷贝给链表。从语义上来看,这需要把更多的精力放在设计上。
确定安装并运行了Visual Studio。
执行以下步骤。
1.创建一个新的空C++控制台应用程序,名为LinkedList
。
2. 添加一个新的头文件CList.h
,并输入下面的代码:
#ifndef _LIST_
#define _LIST_
#include <Windows.h>
template <class T>
class CNode
{
public:
CNode(T* tElement) : tElement(tElement), next(0) { }
T* Element() const { return tElement; }
CNode*& Next(){ return next; }
private:
T* tElement;
CNode* next;
};
template <class T>
class CList
{
public:
CList() : dwCount(0), head(0){ }
CList(T* tElement) : dwCount(1), head(new CNode<T>(tElement)){ }
virtual ~CList(){ }
void Append(CNode<T>*& node, T* tElement);
void Insert(T* tElement);
bool Remove(T* tElement);
DWORD Count() const { return dwCount; }
CNode<T>*& Head() { return head; }
T* GetFirst(){ return head != NULL ? head->Element() : NULL; }
T* GetLast();
T* GetNext(T* tElement);
T* Find(DWORD(*Function)(T* tParameter), DWORD dwValue);
protected:
CList(const CList& list);
CList& operator = (const CList& list);
private:
CNode<T>* head;
DWORD dwCount;
};
template <class T>
void CList<T>::Append(CNode<T>*& node, T* tElement)
{
if (node == NULL)
{
dwCount++;
node = new CNode<T>(tElement);
return;
}
Append(node->Next(), tElement);
}
template <class T>
void CList<T>::Insert(T* tElement)
{
dwCount++;
if (head == NULL)
{
head = new CNode<T>(tElement);
return;
}
CNode<T>* tmp = head;
head = new CNode<T>(tElement);
head->Next() = tmp;
}
template <class T>
bool CList<T>::Remove(T* tElement)
{
if (head == NULL)
{
return NULL;
}
if (head->Element() == tElement)
{
CNode<T>* tmp = head;
head = head->Next();
delete tmp;
dwCount--;
return true;
}
CNode<T>* tmp = head;
CNode<T>* lst = head->Next();
while (lst != NULL)
{
if (lst->Element() == tElement)
{
tmp->Next() = lst->Next();
delete lst;
dwCount--;
return true;
}
lst = lst->Next();
tmp = tmp->Next();
}
return false;
}
template <class T>
T* CList<T>::GetLast()
{
if (head)
{
CNode<T>* tmp = head;
while (tmp->Next())
{
tmp = tmp->Next();
}
return tmp->Element();
}
return NULL;
}
template <class T>
T* CList<T>::GetNext(T* tElement)
{
if (head == NULL)
{
return NULL;
}
if (tElement == NULL)
{
return GetFirst();
}
if (head->Element() == tElement)
{
return head->Next() != NULL ? head->Next()->Element() : NULL;
}
CNode<T>* lst = head->Next();
while (lst != NULL)
{
if (lst->Element() == tElement)
{
return lst->Next() != NULL ? lst->Next()->Element() : NULL;
}
lst = lst->Next();
}
return NULL;
}
template <class T>
T* CList<T>::Find(DWORD(*Function)(T* tParameter), DWORD dwValue)
{
try
{
T* tElement = NULL;
while (tElement = GetNext(tElement))
{
if (Function(tElement) == dwValue)
{
return tElement;
}
}
}
catch (...) {}
return NULL;
}
#endif
3.有了CList
类的实现和定义,创建CQueue
和CStack
就很容易了。先创建CQueue
,右键单击【头文件】,创建一个新的头文件CQueue.h
,并输入下面的代码:
#ifndef _ _QUEUE_ _
#define _ _QUEUE_ _
#include "CList.h"
template<class T>
class CQueue : CList<T>
{
public:
CQueue() : CList<T>(){ }
CQueue(T* tElement) : CList<T>(tElement){ }
virtual ~CQueue(){ }
virtual void Enqueue(T* tElement)
{
Append(Head(), tElement);
}
virtual T* Dequeue()
{
T* tElement = GetFirst();
Remove(tElement);
return tElement;
}
virtual T* Peek()
{
return GetFirst();
}
CList<T>::Count;
protected:
CQueue(const CQueue<T>& cQueue);
CQueue<T>& operator = (const CQueue<T>& cQueue);
};
#endif
4.类似地,再创建CStack
。右键单击【头文件】,创建一个新的头文件CStack.h
,并输入下面的以下代码:
#ifndef _ _STACK_ _
#define _ _STACK_ _
#include "CList.h"
template<class T>
class CStack : CList<T>
{
public:
CStack() : CList<T>(){ }
CStack(T* tElement) : CList<T>(tElement){ }
virtual ~CStack(){ }
virtual void Push(T* tElement)
{
Insert(tElement);
}
virtual T* Pop()
{
T* tElement = GetFirst();
Remove(tElement);
return tElement;
}
virtual T* Peek()
{
return GetFirst();
}
CList<T>::Count;
protected:
CStack(const CStack<T>& cStack);
CStack<T>& operator = (const CStack<T>& cStack);
};
#endif
5. 最后,实现LinkedList.cpp
中的代码,我们用来充当main
例程:
#include <iostream>
using namespace std;
#include "CQueue.h"
#include "CStack.h"
int main()
{
CQueue<int>* cQueue = new CQueue<int>();
CStack<double>* cStack = new CStack<double>();
for (int i = 0; i < 10; i++)
{
cQueue->Enqueue(new int(i));
cStack->Push(new double(i / 10.0));
}
cout << "Queue - integer collection:" << endl;
for (; cQueue->Count();)
{
cout << *cQueue->Dequeue() << " ";
}
cout << endl << endl << "Stack - double collection:" << endl;
for (; cStack->Count();)
{
cout << *cStack->Pop() << " ";
}
delete cQueue;
delete cStack;
cout << endl << endl;
return system("pause");
}
首先,解释一下CList
类。为了更方便地处理,该链表由CNode
类型的元素组成。CNode
类有两个特征:tElement
指针(指向用户自定义的元素)和next
指针(指向链表下一个项);实现了两个方法:Element
和Next
。Element
方法返回当前元素地址的指针,Next
方法返回下一个项地址的引用。
从文字方面看,构造函数称为ctor
,析构函数称为dtor
。CList
的默认构造函数是公有函数,创建一个空的链表。第2个构造函数创建一个包含一个开始元素的链表。具有动态结构的链表必须有析构函数。Append
方法在链表的末尾插入一个元素,也是链表的最后一个元素。Count
方法返回链表当前的元素个数。Head
方法返回链表开始节点的引用。
GetFirst
方法返回链表的第1个元素,如果链表为空,则返回NULL
。GetLast
方法返回链表的最后一个元素,如果链表为空,则返回NULL
。GetNext
方法返回链表的下一个项,即相对于地址由T* tElement
参数提供的项的下一个项。如果未找到该项,GetNext
方法返回NULL
。
Find
方法显然是一个高级特性,针对未定义类型T
和未定义的Function
方法(带tParameter
参数)设计。假设要使用一个包含学生对象的链表,例如迭代数据(如,使用GetNext
方法)或查找某个学生。如果有可能为将来定义的类型实现一个返回unsigned long
类型(DWORD
)的方法,而且该方法要把未知类型数据与dwValue
参数做比较,应该怎么做?例如,假设要根据学生的ID找出这名学生,可以使用下面的代码:
#include <windows.h>
#include "CList.h"
class CStudent
{
public:
CStudent(DWORD dwStudentId) : dwStudentId(dwStudentId){ }
static DWORD GetStudentId(CStudent* student)
{
DWORD dwValue = student->GetId();
return dwValue;
}
DWORD GetId() const
{
return dwStudentId;
}
private:
DWORD dwStudentId;
};
int main()
{
CList<CStudent>* list = new CList<CStudent>();
list->Insert(new CStudent(1));
list->Insert(new CStudent(2));
list->Insert(new CStudent(3));
CStudent* s = list->Find(&CStudent::GetStudentId, 2);
if (s != NULL)
{
// 找到s
}
return 0;
}
如果链表用于处理基本类型(如,int
),可使用下面的代码:
#include <windows.h>
#include "CList.h"
DWORD Predicate(int* iElement)
{
return (DWORD)(*iElement);
}
int main()
{
CList<int>* list = new CList<int>();
list->Insert(new int(1));
list->Insert(new int(2));
list->Insert(new int(3));
int* iElement = list->Find(Predicate, 2);
if (iElement != NULL)
{
// 找到iElement
}
return 0;
}
回到我们的示例。为何要把拷贝构造函数和operator=
都声明为protected
?要知道,我们在这里实现的链表中储存着指针,而这些指针指向那些在链表外的对象。如果用户能随意(或无意)地通过拷贝构造函数或=
运算符来拷贝链表,会非常危险。因此,要把把拷贝构造函数和=
运算符设置为protected
,不让用户使用。为此,必须先把两个函数声明为protected
,然后在需要时再实现它们;否则,编译器在默认情况下会把这两个函数设置为public
,然后逐个拷贝指针,这样做是错误的。把它们声明为private
也不够。在这种情况下,CList
基类的派生类依旧会遇到同样的问题。派生类仍需要要把拷贝构造函数和=
运算符声明为protected
,否则编译器还是会把这些方法默认生成public
。如果基类包含拷贝构造函数和=
运算符,派生类就会默认调用它们,除非派生类能显式调用自己的版本。但是,我们的初衷是让派生类用上CList
基类中的拷贝构造函数和=
运算符,所以将其声明为protected
。
CList
类的private
部分包含了把链表实现为线性链表所需的对象。这意味着链表中的每个元素都指向下一个元素,头节点指向第1个元素。
CQueue
和CStack
类分别实现为队列和栈。不难看出,设计好CList
基类以后(尤其是设计了Enqueue
、Dequeue
、Push
、Pop
和Peek
方法),实现这两个类有多么简单。只要CList
类设计得当,设计CQueue
、CStack
,甚至其他类都非常简单。
[1]译注:即弹出向导窗口后直接点【完成】。
[2]译者注:为了区分attribute和property,本书把attribute译为“特征”,property译为“属性”。
本章介绍以下内容:
现在的计算机能同时处理多件事,许多Windows用户还没有完全意识到这一点。我们举例说明一下。当启动PC系统时,许多进程都在后台启动(例如,管理电子邮件的进程、负责更新病毒库的进程等)。通常,用户在执行其他任务时(如,上网),还会打印文件或播放CD。这些活动都需要管理。支持多进程的多任务系统处理这些情况得心应手。在这种多任务系统中,CPU以极快的速度在各进程间切换,每个进程仅运行几毫秒。从严格意义上来说,CPU在任何时刻只运行一个进程,只不过它快速切换进程营造了并行处理的假象。
近些年来,操作系统演变为一个顺序的概念模型(顺序进程)。包括操作系统在内,所有的可运行软件都在计算机中表现为一系列顺序进程。进程是执行程序的实例。每个进程都有自己的虚拟地址空间和控制线程。线程是操作系统调度器(scheduler)分配处理器时间的基础单元。我们可以把系统看作是运行在准并行环境中的进程集合。在进程(程序)间快速地反复切换叫做多任务处理。
图2.1演示了执行4个程序调度的一个单核CPU多任务处理系统。图2.2演示了执行4个进程的一个多核CPU多任务处理系统,每个进程单独运行,各有一个控制流。
图2.1 单核CPU多任务处理系统
图2.2 多核CPU多任务处理系统
如图2.3所示,随着时间的推移,虽然进程有不同程度的进展,但是在每一时刻,单核CPU只运行一个进程。
图2.3 进程在单核CPU中的运行情况
前面提到过,在传统的操作系统中,每个进程都有一个地址空间和一个控制线程。在许多情况下,一个进程的地址空间中要执行多个线程,在准并行上下文中,这些线程就像是不同的进程一样。有多个线程的主要原因是,许多应用程序都要求能立即执行多项操作。当然,某些操作可以等待(阻塞)一段时间。把运行在准并行上下文中的应用程序分解成多个单独的线程,程序设计模型就变得更简单了。通过添加线程,操作系统提供了一个新特性:并行实体能共享一个地址空间和它们的所有数据。这是执行并发的必要条件。
传统的操作系统必须提供创建进程和终止进程的方法。下面列出了4个引发创建进程的主要事件:
操作系统启动后,会创建多个进程。一些是前台进程,与用户(人)交互,并根据用户的要求执行操作。一些是后台进程,执行特定的功能,与用户行为不相关。例如,可以把接收电子邮件设计成后台进程,因为大部分时间都用不到这一功能,只需在有电子邮件到达时处理即可。后台进程通常处理诸如电子邮件、打印等活动。
在Windows中,CreateProcess
(一个Win32函数调用)负责创建进程和加载进程上下文。欲详细了解CreateProcess
,请参阅MSDN(http://msdn.microsoft.com/en-us/library/windows/desktop/ms682425%28v=vs.85%29.aspx)。我们将在下面的示例中演示基本的进程创建和同步。
确定安装并运行了Visual Studio。
现在,我们按下面的步骤创建一个程序,稍后再详细解释。
1. 创建一个新的默认C++控制台应用程序,名为ProcessDemo
。
2. 打开ProcessDemo.cpp
。
3. 添加下面的代码:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
STARTUPINFO startupInfo = { 0 };
PROCESS_INFORMATION processInformation = { 0 };
BOOL bSuccess = CreateProcess(
TEXT("C:\\Windows\\notepad.exe"), NULL, NULL,
NULL, FALSE, NULL, NULL, NULL, &startupInfo,
&processInformation);
if (bSuccess)
{
cout << "Process started." << endl
<< "Process ID:\t"
<< processInformation.dwProcessId << endl;
}
else
{
cout << "Cannot start process!" << endl
<< "Error code:\t" << GetLastError() << endl;
}
return system("pause");
}
该程序的输出如图2.4所示:
图2.4
如图2.4所示,开始了一个新的进程(记事本)。
CreateProcess
函数用于创建一个新进程及其主线程。新进程在主调进程的安全上下文中运行。
操作系统为进程分配进程标识符。进程标识符用于标识进程,在进程终止之前有效。或者,对于一些API(如OpenProcess
函数),进程标识符用于获得进程的句柄。最初线程的线程标识符由进程分配,该标识符可用于在OpenThread
中打开一个线程的句柄。线程标识符在线程终止之前有效,可作为系统中线程的唯一标识。这些标识符都返回PROCESS_INFORMATION
结构中。
主调线程可以使用WaitForInputIdle
函数,在新进程完成其初始化且正在等待用户输入时等待。这在父进程和子进程的同步中很有用,因为CreateProcess
不会等到新进程完成初始化才返回。例如,创建的进程会在查找与新进程相关的窗口之前使用WaitForInputIdle
函数。
终止进程较好的做法是调用ExitProcess
函数,因为它会给所属进程的所有DLL都发送一条即将终止的通知。而关闭进程的其他方法就不会这样做(如,TerminateProcess
API)。注意,只要进程中有一个线程调用ExitProcess
函数,该进程的其他线程都会立即终止,根本没机会执行其他代码(包括相关DLL的线程终止代码)。
虽然每个进程都是一个独立的实体,有各自的指令指针和内部状态,但是进程之间也要经常交互。一个进程生成的输出数据可能是另一个进程所需的输入数据。根据两个进程的相对运行速度,可能会发生这种情况:读操作已准备运行,但是却没有输入。在能读到输入数据之前,该进程必定被阻塞。从逻辑上看,如果进程被阻塞就不能继续运行,因为该进程正在等待尚未获得的输入。如果操作系统在这时决定把CPU暂时分配给另一个进程,正在等待的进程就有可能停止。这是两种完全不同的情况。第一种情况是问题本身造成的(即,在用户键入数据之前无法解析用户的命令行);而第二种情况是系统的技术原因造成的(即,进程用完了分配给它的时间,又没有足够的CUP能单独运行该线程)。
图2.5中的状态图演示了一个进程可能处于的3种状态。
图2.5 进程的三种状态
运行和就绪状态有些类似。处于这两种状态的进程都可以运行,只是在就绪状态中,进程暂时没有CPU可用。阻塞状态与前两种状态不同,在阻塞状态中,即使CPU空闲,进程也不能运行。
假设有进程A。当调度器选择另一个进程在CPU上执行时,进程A发生转换过程2。当调度器选择执行进程A时,发生转换过程3。为了等待输入,转换过程1把进程A设置为阻塞状态。当进程A获得所需的输入时,发生转换过程4。
在现代的多任务系统中,进程控制块(Process Control Block,PCB)储存了高效管理进程所需的许多不同数据项。PCB是操作系统为了管理进程,在内核中设置的一种的数据结构。操作系统中的进程用PCB来表示。虽然这种数据结构的细节因系统而异,但是常见的部分大致可分为三大类:
图2.6
PCB是管理进程的中心。绝大多数操作系统程序(包括那些与调度、内存、I/O资源访问和性能监控相关的程序)都要访问和修改它。通常,要根据PCB为进程构建数据。例如,某PCB内指向其他PCB的指针以不同的调度状态(就绪、阻塞等)创建进程队列。
操作系统必须代表进程来管理资源。它必须不断地关注每个进程的状态、系统资源和内部值。下面的程序示例演示了如何获得一个进程基本信息结构地址,其中的一个特征就是PCB的地址。另一个特征是唯一的进程ID。为简化示例,我们只输出从对象中读取的进程ID。
确定安装并运行了Visual Studio。
我们再来创建一个操控进程的程序。这次,我们从进程基本信息结构中获取进程ID。请执行以下步骤。
1. 创建一个新的默认C++控制台应用程序,名为NtProcessDemo
。
2. 打开NtProcessDemo.cpp
。
3. 添加下面的代码:
#include "stdafx.h"
#include <Windows.h>
#include <Winternl.h>
#include <iostream>
using namespace std;
typedef NTSTATUS(NTAPI* QEURYINFORMATIONPROCESS)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
int _tmain(int argc, _TCHAR* argv[])
{
STARTUPINFO startupInfo = { 0 };
PROCESS_INFORMATION processInformation = { 0 };
BOOL bSuccess = CreateProcess(
TEXT("C:\\Windows\\notepad.exe"), NULL, NULL,
NULL, FALSE, NULL, NULL, NULL, &startupInfo,
&processInformation);
if (bSuccess)
{
cout << "Process started." << endl << "Process ID:\t"
<< processInformation.dwProcessId << endl;
PROCESS_BASIC_INFORMATION pbi;
ULONG uLength = 0;
HMODULE hDll = LoadLibrary(
TEXT("C:\\Windows\\System32\\ntdll.dll"));
if (hDll)
{
QEURYINFORMATIONPROCESS QueryInformationProcess =
(QEURYINFORMATIONPROCESS)GetProcAddress(
hDll, "NtQueryInformationProcess");
if (QueryInformationProcess)
{
NTSTATUS ntStatus = QueryInformationProcess(
processInformation.hProcess,
PROCESSINFOCLASS::ProcessBasicInformation,
&pbi, sizeof(pbi), &uLength);
if (NT_SUCCESS(ntStatus))
{
cout << "Process ID (from PCB):\t"
<< pbi.UniqueProcessId << endl;
}
else
{
cout << "Cannot open PCB!" << endl
<< "Error code:\t" << GetLastError()
<< endl;
}
}
else
{
cout << "Cannot get "
<< "NtQueryInformationProcess function!"
<< endl << "Error code:\t"
<< GetLastError() << endl;
}
FreeLibrary(hDll);
}
else
{
cout << "Cannot load ntdll.dll!" << endl
<< "Error code:\t" << GetLastError() << endl;
}
}
else
{
cout << "Cannot start process!" << endl
<< "Error code:\t" << GetLastError() << endl;
}
return 0;
}
该例中,我们使用了一些其他头文件:Winternl.h
和Windows.h
。Winternl.h
头文件包含了大部分Windows内部函数的原型和数据表示,例如PROCESS_BASIC_INFORMATION
结构的定义:
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress;
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
操作系统在调用内核态和用户态之间的子例程时会用到该结构。
结合PROCESSINFOCLASS::ProcessBasicInformation
枚举,我们通过UniqueProcessId
特征获取进程标识符,如上面的代码所示。
首先,定义QEURYINFORMATIONPROCESS
,这是从ntdll.dll
中加载的NtQueryInformationProcess
函数的别名。当通过GetProcAddress
Win32 API获得该函数的地址时,就可以询问PROCESS_BASIC_INFORMATION
对象了。注意PROCESS_BASIC_INFORMATION
结构的PebBaseAddress字段是一个指针,指向新创建进程的PCB。如果还想进一步研究PCB,检查新创建的进程,则必须在运行时使用ReadProcessMemory
例程。因为PebBaseAddress
指向属于新创建进程的内存。
进程之间的通信非常重要。虽然操作系统提供了进程间通信的机制,但是在介绍这些机制之前,我们先来考虑一些与之相关的问题。如果航空预定系统中有两个进程在同时销售本次航班的最后一张机票,怎么办?这里要解决两个问题。第1个问题是,一个座位不可能卖两次。第2个问题是一个依赖性问题:如果进程A生成的某些数据是进程B需要读取的(如,打印这些数据),那么进程B在进程A准备好这些数据之前必须一直等待。进程和线程的不同在于,线程共享同一个地址空间,而进程拥有单独的地址空间。因此,用线程解决第1个问题比较容易。至于第2个问题,线程也同样能解决。所以,理解同步机制非常重要。
在讨论IPC之前,我们先来考虑一个简单的例子:CD刻录机。当一个进程要刻录一些内容时,会在特定的刻录缓冲区中设置文件句柄(我们立刻要刻录更多的文件)。另一个负责刻录的进程,检查待刻录的文件是否存在,如果存在,该进程将刻录文件,然后从缓冲区中移除该文件的句柄。假设刻录缓冲区有足够多的索引,分别编号为I0、I1、I2等,每个索引都能储存若干文件句柄。再假设有两个共享变量:p_next
和p_free
,前者指向下一个待刻录的缓冲区索引,后者指向缓冲区中的下一个空闲索引。所有进程都要使用这两个变量。在某一时刻,索引I0和I2为空(即文件已经刻录完毕),I3和I5已经加入缓冲。同时,进程5和进程6决定把文件句柄加入队列准备刻录文件。这一状况如图2.7所示。
图2.7
首先,进程5读取p_free
,把它的值I6储存在自己的局部变量f_slot
中。接着,发生了一个时钟中断,CPU认为进程5运行得太久了,决定转而执行进程6。然后,进程6也读取p_free
,同样也把I6储存在自己的局部变量f_slot
中。此时,两个进程都认为下一个可用的索引是I6。进程6现在继续运行,它把待拷贝文件的句柄储存在索引I6中,并更新p_free
为I7。然后,系统让进程6睡眠。现在,进程5从原来暂停的地方再次开始运行。它查看自己的f_slot
,发现可用的索引是I6,于是把自己待拷贝文件的句柄写到索引I6上,擦除了进程6刚写入的文件句柄。然后,进程5计算f_slot+1
得I7,就把p_free
设置为I7。现在,刻录缓冲区内部保持一致,所以刻录进程并未出现任何错误。但是,进程6再也接收不到任何输出。
进程6将被无限闲置,等待着再也不会有的输出。像这样两个或更多实体读取或写入某共享数据的情况,最终的结果取决于进程的执行顺序(即何时执行哪一个进程),这叫做竞态条件(race condition)。
如何避免竞态条件?大部分解决方案都涉及共享内存、共享文件以及避免不同的进程同时读写共享数据。换句话说,我们需要互斥(mutual exclusion)或一种能提供独占访问共享对象的机制(无论它是共享变量、共享文件还是其他对象)。当进程6开始使用进程5刚用完的一个共享对象时,就会发生糟糕的事情。
程序中能被访问共享内存的部分叫做临界区(critical section)。为了避免竞态条件,必须确保一次只能有一个进程进入临界区。这种方法虽然可以避免竞态条件,但是在执行并行进程时会影响效率,毕竟并行的目的是正确且高效地合作。要使用共享数据,必须处理好下面4个条件:
以上所述如图2.8所示。过程A在T1时进入临界区。稍后,进程B在T2尝试进入其临界区,但是失败。因为另一个进程已经在临界区中,同一时间内只允许一个进程在临界区内。在T3之前,进程B必须被临时挂起。在进程A离开临界区时,进程B便可立即进入。最终,进程B离开临界区(T4时),又回到没有进程进入临界区的状态。
图2.8
下面是一个进程间通信的程序示例。我们创建的这个程序一开始就有两个进程,它们要在一个普通窗口中完成绘制矩形的任务。从某种程度上看,这两个进程需要相互通信,即当一个进程正在画矩形时,另一个进程要等待。
确定安装并运行了Visual Studio。
1. 创建一个新的默认C++控制台应用程序,命名为IPCDemo
。
2. 右键单击【解决方案资源管理器】,并选择【添加】-【新建项目】。选择C++【Win32控制台应用程序】,添加一个新的默认C++控制台应用程序,命名为IPCWorker
。
3. 在IPCWorker.cpp
文件中输入下面的代码:
#include "stdafx.h"
#include <Windows.h>
#define COMMUNICATION_OBJECT_NAME TEXT("__FILE_MAPPING__")
#define SYNCHRONIZING_MUTEX_NAME TEXT( "__TEST_MUTEX__" )
typedef struct _tagCOMMUNICATIONOBJECT
{
HWND hWndClient;
BOOL bExitLoop;
LONG lSleepTimeout;
} COMMUNICATIONOBJECT, *PCOMMUNICATIONOBJECT;
int _tmain(int argc, _TCHAR* argv[])
{
HBRUSH hBrush = NULL;
if (_tcscmp(TEXT("blue"), argv[0]) == 0)
{
hBrush = CreateSolidBrush(RGB(0, 0, 255));
}
else
{
hBrush = CreateSolidBrush(RGB(255, 0, 0));
}
HWND hWnd = NULL;
HDC hDC = NULL;
RECT rectClient = { 0 };
LONG lWaitTimeout = 0;
HANDLE hMapping = NULL;
PCOMMUNICATIONOBJECT pCommObject = NULL;
BOOL bContinueLoop = TRUE;
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, SYNCHRONIZING_MUTEX_NAME);
hMapping = OpenFileMapping(FILE_MAP_READ, FALSE, COMMUNICATION_OBJECT_NAME);
if (hMapping)
{
while (bContinueLoop)
{
WaitForSingleObject(hMutex, INFINITE);
pCommObject = (PCOMMUNICATIONOBJECT)MapViewOfFile(hMapping,
FILE_MAP_READ, 0, 0, sizeof(COMMUNICATIONOBJECT));
if (pCommObject)
{
bContinueLoop = !pCommObject->bExitLoop;
hWnd = pCommObject->hWndClient;
lWaitTimeout = pCommObject->lSleepTimeout;
UnmapViewOfFile(pCommObject);
hDC = GetDC(hWnd);
if (GetClientRect(hWnd, &rectClient))
{
FillRect(hDC, &rectClient, hBrush);
}
ReleaseDC(hWnd, hDC);
Sleep(lWaitTimeout);
}
ReleaseMutex(hMutex);
}
}
CloseHandle(hMapping);
CloseHandle(hMutex);
DeleteObject(hBrush);
return 0;
}
4. 打开IPCDemo.cpp
,并输入下面的代码:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
using namespace std;
#define COMMUNICATION_OBJECT_NAME TEXT("_ _FILE_MAPPING_ _")
#define SYNCHRONIZING_MUTEX_NAME TEXT( "_ _TEST_MUTEX_ _" )
#define WINDOW_CLASS_NAME TEXT( "_ _TMPWNDCLASS_ _" )
#define BUTTON_CLOSE 100
typedef struct _tagCOMMUNICATIONOBJECT
{
HWND hWndClient;
BOOL bExitLoop;
LONG lSleepTimeout;
} COMMUNICATIONOBJECT, *PCOMMUNICATIONOBJECT;
LRESULT CALLBACK WndProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
HWND InitializeWnd();
PCOMMUNICATIONOBJECT pCommObject = NULL;
HANDLE hMapping = NULL;
int _tmain(int argc, _TCHAR* argv[])
{
cout << "Interprocess communication demo." << endl;
HWND hWnd = InitializeWnd();
if (!hWnd)
{
cout << "Cannot create window!" << endl << "Error:\t" <<
GetLastError() << endl;
return 1;
}
HANDLE hMutex = CreateMutex(NULL, FALSE, SYNCHRONIZING_MUTEX_NAME);
if (!hMutex)
{
cout << "Cannot create mutex!" << endl << "Error:\t" <<
GetLastError() << endl;
return 1;
}
hMapping = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE, 0,
sizeof(COMMUNICATIONOBJECT), COMMUNICATION_OBJECT_NAME);
if (!hMapping)
{
cout << "Cannot create mapping object!" << endl << "Error:\t"
<< GetLastError() << endl;
return 1;
}
pCommObject = (PCOMMUNICATIONOBJECT)MapViewOfFile(hMapping,
FILE_MAP_WRITE, 0, 0, 0);
if (pCommObject)
{
pCommObject->bExitLoop = FALSE;
pCommObject->hWndClient = hWnd;
pCommObject->lSleepTimeout = 250;
UnmapViewOfFile(pCommObject);
}
STARTUPINFO startupInfoRed = { 0 };
PROCESS_INFORMATION processInformationRed = { 0 };
STARTUPINFO startupInfoBlue = { 0 };
PROCESS_INFORMATION processInformationBlue = { 0 };
BOOL bSuccess = CreateProcess(TEXT("..\\Debug\\IPCWorker.exe"),
TEXT("red"), NULL, NULL, FALSE, 0, NULL, NULL, &startupInfoRed,
&processInformationRed);
if (!bSuccess)
{
cout << "Cannot create process red!" << endl << "Error:\t" <<
GetLastError() << endl;
return 1;
}
bSuccess = CreateProcess(TEXT("..\\Debug\\IPCWorker.exe"),
TEXT("blue"), NULL, NULL, FALSE, 0, NULL, NULL, &startupInfoBlue,
&processInformationBlue);
if (!bSuccess)
{
cout << "Cannot create process blue!" << endl << "Error:\t" <<
GetLastError() << endl;
return 1;
}
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
UnregisterClass(WINDOW_CLASS_NAME, GetModuleHandle(NULL));
CloseHandle(hMapping);
CloseHandle(hMutex);
cout << "End program." << endl;
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_COMMAND:
{
switch (LOWORD(wParam))
{
case BUTTON_CLOSE:
{
PostMessage(hWnd, WM_CLOSE, 0, 0);
break;
}
}
break;
}
case WM_DESTROY:
{
pCommObject = (PCOMMUNICATIONOBJECT)MapViewOfFile(hMapping,
FILE_MAP_WRITE, 0, 0, 0);
if (pCommObject)
{
pCommObject->bExitLoop = TRUE;
UnmapViewOfFile(pCommObject);
}
PostQuitMessage(0);
break;
}
default:
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return 0;
}
HWND InitializeWnd()
{
WNDCLASSEX wndEx;
wndEx.cbSize = sizeof(WNDCLASSEX);
wndEx.style = CS_HREDRAW | CS_VREDRAW;
wndEx.lpfnWndProc = WndProc;
wndEx.cbClsExtra = 0;
wndEx.cbWndExtra = 0;
wndEx.hInstance = GetModuleHandle(NULL);
wndEx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wndEx.lpszMenuName = NULL;
wndEx.lpszClassName = WINDOW_CLASS_NAME;
wndEx.hCursor = LoadCursor(NULL, IDC_ARROW);
wndEx.hIcon = LoadIcon(wndEx.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wndEx.hIconSm = LoadIcon(wndEx.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
if (!RegisterClassEx(&wndEx))
{
return NULL;
}
HWND hWnd = CreateWindow(wndEx.lpszClassName,
TEXT("Interprocess communication Demo"),
WS_OVERLAPPEDWINDOW, 200, 200, 400, 300, NULL, NULL,
wndEx.hInstance, NULL);
if (!hWnd)
{
return NULL;
}
HWND hButton = CreateWindow(TEXT("BUTTON"), TEXT("Close"),
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP,
275, 225, 100, 25, hWnd, (HMENU)BUTTON_CLOSE, wndEx.hInstance,
NULL);
HWND hStatic = CreateWindow(TEXT("STATIC"), TEXT(""), WS_CHILD |
WS_VISIBLE, 10, 10, 365, 205, hWnd, NULL, wndEx.hInstance, NULL);
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
return hStatic;
}
这次演示的示例有点难。我们需要两个单独的线程,所以在同一个解决方案中创建了两个项目。
为了简化这个示例,我们在主应用程序IPCDemo
中创建了两个进程,IPCDemo
将在应用程序窗口中绘制一个区域。如果没有正确的通信和进程同步,就会发生多路访问共享资源的情况。考虑到操作系统会在进程间快速切换,而且大部分PC都有多核CPU,这很可能会导致两个进程同时画一个区域,即多个进程同时访问未保护的区域。先来看IPCWorker
,这个名称的意思是,需要进程为我们处理一些工作。
我们使用了一个映射对象(即,内存中为进程分配读取或写入的区域)。IPCWorker
或简称Worker
,要请求获得一个已命名的互斥量。如果获得互斥量,该进程就能处理并获取一个指向内存区域(文件映射)的指针,信息将储存在这个区域。必须获得互斥量,才能进行独占访问。进程在WaitForSingleObject
返回后获得互斥量。请看下面的语句:
HANDLE hMutex = OpenMutex( MUTEX_ALL_ACCESS, FALSE, SYNCHRONIZING_MUTEX_NAME );
我们要为互斥量(hMutex
)分配一个句柄,调用OpenMutex
Win32 API获得该已命名互斥量的句柄(如果有互斥量的话)。请看下面的语句:
WaitForSingleObject( hMutex, INFINITE );
执行完这条语句后,当WaitForSingleObject
API返回时继续执行。
pCommObject = ( PCOMMUNICATIONOBJECT )
MapViewOfFile( hMapping, FILE_MAP_READ, 0, 0, sizeof( COMMUNICATIONOBJECT ) );
调用MapViewOfFile
Win32 API获得指向文件映射对象的句柄(指针)。现在,进程可以从共享内存对象中读取并获得所需的信息了。该进程要读取bExitLoop
变量才能获悉是否继续执行。然后,该进程要读取待绘制区域窗口的句柄(hWnd
)。最后,还需要lSleepTimeout
变量记录进程睡眠多久。我们故意添加了sleep
时间,因为进程间切换太快根本注意不到。
ReleaseMutex( hMutex );
调用ReleaseMutex
Win32 API释放互斥量的所有权,让其他进程可以获得互斥量,继续执行其他任务。分析完IPCWorker
,我们来看IPCDemo
项目。该项目定义了_tagCOMMUNICATIONOBJECT
结构,用于整个文件映射过程中对象之间的通信。
文件映射(file mapping)是把文件的内容与一个进程的一部分虚拟地址空间相关联。操作系统创建一个文件映射对象(也叫做区域对象[section object])来维护这种关联。文件视图(file view)是进程用于访问文件内容的虚拟地址空间部分。有了文件映射,进程不仅能使用随机I/O和顺序I/O,而且无需把整个文件映射到内存中就能高效地使用大型数据文件(如,数据库)。多个进程还可以用已映射的内存文件来共享数据。详见MSDN(http://msdn.microsoft.com/en-us/library/windows/desktop/aa366883%28v=vs.85%29.aspx)。
正是因为IPCDemo
在运行Worker
进程之前就创建了文件映射,所以从Worker
进程询问文件映射之前不用检查文件映射是否存在。IPCDemo
创建并初始化应用程序窗口和待绘制区域后,创建了一个已命名的互斥量和文件映射。然后,用不同的命令行参数(用以区别)创建不同的进程。
WndProc
例程处理WM_COMMAND
和WM_DESTROY
消息。当我们需要通知应用程序安全地关闭时,WM_COMMAND
触发按钮按下事件,而WM_DESTROY
则释放用过的文件映射,并向主线程消息队列寄送关闭消息:
PostQuitMessage( 0 );
文件映射要与常驻磁盘的文件和常驻内存的文件视图一起运作。用内存的文件视图比用硬盘驱动的读写速度快。如果要用共享对象在进程之间处理一些简单的事情,选用文件映射是很好的编程习惯。如果把CreateFileMapping
API的第1个参数设置为-1
,磁盘中就不会有文件存在:
CreateFileMapping( ( HANDLE ) -1, NULL, PAGE_READWRITE, 0,
sizeof( COMMUNICATIONOBJECT ), COMMUNICATION_OBJECT_NAME );
这真是再好不过了,因为我们正打算使用一部分内存,这样更快,而且也够用了。
调用IPCWorker
进程时要注意。像下面这样设置CreateProcess
,以供调试:
bSuccess = CreateProcess( TEXT( "..\\Debug\\IPCWorker.exe" ),
TEXT( "red" ), NULL, NULL, FALSE, 0, NULL, NULL,
&startupInfoRed, &processInformationRed );
Visual Studio在调试模式中只会从项目文件夹开始启动,不会从程序的exe
文件夹开始启动。而且,Visual Studio默认把所有的Win32项目都输出到同一个文件夹中。所以,在文件路径中,我们必须从项目文件夹返回上一级(文件夹),然后找到Debug文件夹,整个项目的输出(exe
)就在这个文件夹中。如果不想让VS这样启动exe
,就必须改变CreateProcess
调用的路径,或者添加通过命令行或其他类似方法访问文件路径的功能。
进程间通信非常重要,它的实现也很复杂。操作系统的设计人员和开发人员要面临各种问题。接下来,我们讲解一些最常见的问题。
本节讨论的哲学家就餐问题的定义,选自Andrew S. Tanenbaum所著的Mordern Operating Systems(《现代操作系统》)第三版。作者在书中提供了解决方案。
1965年,Dijkstra提出并解决了一个同步问题,他称之为哲学家就餐问题。这个问题简单地描述如下:5位哲学家围坐在一张圆桌边。每位哲学家前面都放着一盘意大利面条。面条很滑,要用两个餐叉才吃得到。相邻两个盘子之间有一个餐叉。桌上的布局如图2.9所示。
图2.9 哲学家就餐问题
假设哲学家的状态是吃面条和思考交替进行。如果哲学家饿了,就会尝试拿起他左边和右边的餐叉,一次只能拿一把。如果成功获得两把餐叉,他就吃一会意大利面,然后放下餐叉,继续思考。关键问题是:能否写一个程序,描述每位哲学家应该怎么做才一定不会卡壳?
我们可以等指定的餐叉可用时才去拿。不过,这样想显然是错误的。如果5位哲学家都同时拿起左边的餐叉,就没人能拿到右边的餐叉,这就出现了死锁。
我们可以修改一下程序,在拿起左边餐叉后,程序检查右边的餐叉是否可用。如果不可用,该哲学家就放下已拿起的左边餐叉,等待一段时间,再重复这一过程。虽然这个解法和上一个解法不同,但是好不到哪里去,也是错误的。如果很不巧,所有的哲学家都同时以该算法开始,拿起他们左边的餐叉,发现右边餐叉不可用,然后放下左边餐叉,等待一会,又同时拿起左边的餐叉……这样永无止尽。这种所有程序无限期不停运行却没有任何进展的情况,叫做饥饿(starvation)。
要实现既不会发生死锁也不会发生饥饿,就要保护“思考”(通过互斥量调用)后面的5个语句。哲学家在开始拿起餐叉之前,要先询问互斥量。前面介绍过,互斥量代表相互排斥或者能给对象提供独占访问。在放下餐叉后,哲学家要释放互斥量。理论上,这种解决方案可行。但这实际上有一个性能问题:在任意时刻,只有一个哲学家进餐。桌上有5个餐叉可用,应该能让两个哲学家同时进餐。
下面给出了完整的解决方案。
确定安装并运行了Visual Studio。
1. 创建一个新的默认Win32项目,命名为PhilosophersDinner
。
2. 打开stdafx.h
,并输入下面的代码:
#pragma once
#include "targetver.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <commctrl.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#include <stdio.h>
#pragma comment ( lib, "comctl32.lib" )
#pragma comment ( linker, "\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' \
version='6.0.0.0' processorArchitecture='*' \
publicKeyToken='6595b64144ccf1df' language='*'\"" )
3. 打开PhilosophersDinner.cpp
,并输入下面的代码:
#include "stdafx.h"
#define BUTTON_CLOSE 100
#define PHILOSOPHER_COUNT 5
#define WM_INVALIDATE WM_USER + 1
typedef struct _tagCOMMUNICATIONOBJECT
{
HWND hWnd;
bool bExitApplication;
int iPhilosopherArray[PHILOSOPHER_COUNT];
int PhilosopherCount;
} COMMUNICATIONOBJECT, *PCOMMUNICATIONOBJECT;
HWND InitInstance(HINSTANCE hInstance, int nCmdShow);
ATOM MyRegisterClass(HINSTANCE hInstance);
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam,
LPARAM lParam);
int PhilosopherPass(int iPhilosopher);
void FillEllipse(HWND hWnd, HDC hDC, int iLeft, int iTop, int
iRight, int iBottom, int iPass);
TCHAR* szTitle = TEXT("Philosophers Dinner Demo");
TCHAR* szWindowClass = TEXT("__PD_WND_CLASS__");
TCHAR* szSemaphoreName = TEXT("__PD_SEMAPHORE__");
TCHAR* szMappingName = TEXT("__SHARED_FILE_MAPPING__");
PCOMMUNICATIONOBJECT pCommObject = NULL;
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE
hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
HANDLE hMapping = CreateFileMapping((HANDLE)-1, NULL,
PAGE_READWRITE, 0, sizeof(COMMUNICATIONOBJECT), szMappingName);
if (!hMapping)
{
MessageBox(NULL, TEXT("Cannot open file mapping"),
TEXT("Error!"), MB_OK);
return 1;
}
pCommObject = (PCOMMUNICATIONOBJECT)MapViewOfFile(hMapping,
FILE_MAP_ALL_ACCESS, 0, 0, 0);
if (!pCommObject)
{
MessageBox(NULL, TEXT("Cannot get access to file mapping! "),
TEXT("Error!"), MB_OK);
CloseHandle(hMapping);
return 1;
}
InitCommonControls();
MyRegisterClass(hInstance);
HWND hWnd = NULL;
if (!(hWnd = InitInstance(hInstance, nCmdShow)))
{
return FALSE;
}
pCommObject->bExitApplication = false;
pCommObject->hWnd = hWnd;
memset(pCommObject->iPhilosopherArray, 0,
sizeof(*pCommObject->iPhilosopherArray));
pCommObject->PhilosopherCount = PHILOSOPHER_COUNT;
HANDLE hSemaphore = CreateSemaphore(NULL,
int(PHILOSOPHER_COUNT / 2), int(PHILOSOPHER_COUNT / 2),
szSemaphoreName);
STARTUPINFO startupInfo[PHILOSOPHER_COUNT] =
{ { 0 }, { 0 }, { 0 }, { 0 }, { 0 } };
PROCESS_INFORMATION processInformation[PHILOSOPHER_COUNT] =
{ { 0 }, { 0 }, { 0 }, { 0 }, { 0 } };
HANDLE hProcesses[PHILOSOPHER_COUNT];
TCHAR szBuffer[8];
for (int iIndex = 0; iIndex < PHILOSOPHER_COUNT; iIndex++)
{
#ifdef UNICODE
wsprintf(szBuffer, L"%d", iIndex);
#else
sprintf(szBuffer, "%d", iIndex);
#endif
if (CreateProcess(TEXT("..\\Debug\\Philosopher.exe"),
szBuffer, NULL, NULL,
FALSE, 0, NULL, NULL, &startupInfo[iIndex],
&processInformation[iIndex]))
{
hProcesses[iIndex] = processInformation[iIndex].hProcess;
}
}
MSG msg = { 0 };
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
pCommObject->bExitApplication = true;
UnmapViewOfFile(pCommObject);
WaitForMultipleObjects(PHILOSOPHER_COUNT, hProcesses, TRUE, INFINITE);
for (int iIndex = 0; iIndex < PHILOSOPHER_COUNT; iIndex++)
{
CloseHandle(hProcesses[iIndex]);
}
CloseHandle(hSemaphore);
CloseHandle(hMapping);
return (int)msg.wParam;
}
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wndEx;
wndEx.cbSize = sizeof(WNDCLASSEX);
wndEx.style = CS_HREDRAW | CS_VREDRAW;
wndEx.lpfnWndProc = WndProc;
wndEx.cbClsExtra = 0;
wndEx.cbWndExtra = 0;
wndEx.hInstance = hInstance;
wndEx.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wndEx.hCursor = LoadCursor(NULL, IDC_ARROW);
wndEx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wndEx.lpszMenuName = NULL;
wndEx.lpszClassName = szWindowClass;
wndEx.hIconSm = LoadIcon(wndEx.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
return RegisterClassEx(&wndEx);
}
HWND InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPED
| WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, 200, 200, 540, 590,
NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return NULL;
}
HFONT hFont = CreateFont(14, 0, 0, 0, FW_NORMAL, FALSE, FALSE,
FALSE, BALTIC_CHARSET, OUT_DEFAULT_PRECIS,
CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH |
FF_MODERN, TEXT("Microsoft Sans Serif"));
HWND hButton = CreateWindow(TEXT("BUTTON"), TEXT("Close"),
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP, 410, 520, 100,
25, hWnd, (HMENU)BUTTON_CLOSE, hInstance, NULL);
SendMessage(hButton, WM_SETFONT, (WPARAM)hFont, TRUE);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return hWnd;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_COMMAND:
{
switch (LOWORD(wParam))
{
case BUTTON_CLOSE:
{
DestroyWindow(hWnd);
break;
}
}
break;
}
case WM_INVALIDATE:
{
InvalidateRect(hWnd, NULL, TRUE);
break;
}
case WM_PAINT:
{
PAINTSTRUCT paintStruct;
HDC hDC = BeginPaint(hWnd, &paintStruct);
FillEllipse(hWnd, hDC, 210, 10, 310, 110,
PhilosopherPass(1));
FillEllipse(hWnd, hDC, 410, 170, 510, 270,
PhilosopherPass(2));
FillEllipse(hWnd, hDC, 335, 400, 435, 500,
PhilosopherPass(3));
FillEllipse(hWnd, hDC, 80, 400, 180, 500,
PhilosopherPass(4));
FillEllipse(hWnd, hDC, 10, 170, 110, 270,
PhilosopherPass(5));
EndPaint(hWnd, &paintStruct);
break;
}
case WM_DESTROY:
{
PostQuitMessage(0);
break;
}
default:
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return 0;
}
int PhilosopherPass(int iPhilosopher)
{
return pCommObject->iPhilosopherArray[iPhilosopher - 1];
}
void FillEllipse(HWND hWnd, HDC hDC, int iLeft, int iTop, int
iRight, int iBottom, int iPass)
{
HBRUSH hBrush = NULL;
if (iPass)
{
hBrush = CreateSolidBrush(RGB(255, 0, 0));
}
else
{
hBrush = CreateSolidBrush(RGB(255, 255, 255));
}
HBRUSH hOldBrush = (HBRUSH)SelectObject(hDC, hBrush);
Ellipse(hDC, iLeft, iTop, iRight, iBottom);
SelectObject(hDC, hOldBrush);
DeleteObject(hBrush);
}
4.右键单击【解决方案资源管理器】,并添加一个新的默认Win32控制台应用程序,命名为Philosopher
。
5. 打开stdafx.h
,并输入下面的代码:
#pragma once
#include "targetver.h"
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
6. 打开Philosopher.cpp
,并输入下面的代码:
#include "stdafx.h"
#include <Windows.h>
#define EATING_TIME 1000
#define PHILOSOPHER_COUNT 5
#define WM_INVALIDATE WM_USER + 1
typedef struct _tagCOMMUNICATIONOBJECT
{
HWND hWnd;
bool bExitApplication;
int iPhilosopherArray[PHILOSOPHER_COUNT];
int PhilosopherCount;
} COMMUNICATIONOBJECT, *PCOMMUNICATIONOBJECT;
void Eat();
TCHAR* szSemaphoreName = TEXT("__PD_SEMAPHORE__");
TCHAR* szMappingName = TEXT("__SHARED_FILE_MAPPING__");
bool bExitApplication = false;
int _tmain(int argc, _TCHAR* argv[])
{
HWND hConsole = GetConsoleWindow();
ShowWindow(hConsole, SW_HIDE);
int iIndex = (int)_tcstol(argv[0], NULL, 10);
HANDLE hMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE,
szMappingName);
while (!bExitApplication)
{
HANDLE hSemaphore = OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE,
szSemaphoreName);
WaitForSingleObject(hSemaphore, INFINITE);
PCOMMUNICATIONOBJECT pCommObject = (PCOMMUNICATIONOBJECT)
MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS, 0, 0,
sizeof(COMMUNICATIONOBJECT));
bExitApplication = pCommObject->bExitApplication;
if (!pCommObject->iPhilosopherArray[
(iIndex + pCommObject->PhilosopherCount - 1)
% pCommObject->PhilosopherCount]
&& !pCommObject->iPhilosopherArray[
(iIndex + 1) % pCommObject->PhilosopherCount])
{
pCommObject->iPhilosopherArray[iIndex] = 1;
Eat();
}
SendMessage(pCommObject->hWnd, WM_INVALIDATE, 0, 0);
pCommObject->iPhilosopherArray[iIndex] = 0;
UnmapViewOfFile(pCommObject);
ReleaseSemaphore(hSemaphore, 1, NULL);
CloseHandle(hSemaphore);
}
CloseHandle(hMapping);
return 0;
}
void Eat()
{
Sleep(EATING_TIME);
}
我们要创建5个进程来模仿5位哲学家的行为。每位哲学家(即,每个进程)都必须思考和进餐。哲学家需要两把餐叉才能进餐,在餐叉可用的前提下,他必须先拿起左边的餐叉,再拿起右边的餐叉。如果两把餐叉都可用,他就能顺利进餐;如果另一把餐叉不可用,他就放下已拿起的左边餐叉,并等待下一次进餐。我们在程序中把进餐时间设置为1秒。
PhilosophersDinner
是该主应用程序。我们创建了文件映射,可以与其他进程通信。创建Semaphore
对象同步进程也很重要。根据前面的分析,使用互斥量能确保同一时刻只有一位哲学家进餐。这种虽然方法可行,但是优化得不够。如果每位哲学家需要两把餐叉才能进餐,可以用FLOOR( NUMBER_OF_PHILOSOPHERS / 2 )
实现两位哲学家同时进餐。这就是我们设置同一时刻最多有两个对象可以传递信号量的原因,如下代码所示:
HANDLE hSemaphore = CreateSemaphore( NULL,
int( PHILOSOPHER_COUNT / 2 ),
int( PHILOSOPHER_COUNT / 2 ), szSemaphoreName );
这里注意,信号量最初可以允许一定数量的对象通过,但是通过CreateSemaphore
API的第3个参数可以递增这个数量。不过在我们的示例中,用不到这个特性。
初始化信号量对象后,就创建了进程,应用程序可以进入消息循环了。分析完PhilosophersDinner
,我们来看Philosopher
应用程序。这是一个控制台应用程序,因为我们不需要接口,所以将隐藏它的主窗口(本例中是控制台)。如下代码所示:
HWND hConsole = GetConsoleWindow( );
ShowWindow( hConsole, SW_HIDE );
接下来,该应用程序要获得它的索引(哲学家的姓名):
int iIndex = ( int ) _tcstol( argv[ 0 ], NULL, 10 );
然后,哲学家必须获得文件映射对象的句柄,并进入消息循环。在消息循环中,哲学家询问传递过来的信号量对象,等待轮到他们进餐。当哲学家获得一个传入的信号量时,就可以获得两把餐叉。然后,通过下面的SendMessage
API,发送一条消息,更新主应用程序的用户接口:
SendMessage( pCommObject->hWnd, WM_INVALIDATE, 0, 0 );
所有的工作完成后,哲学家会释放信号量对象并继续执行。
还有一些经典的IPC问题,如“睡觉的理发师”和“生产者-消费者”问题。本章后面会给出“生产者-消费者”问题的解法。
我们可以把进程看作是一个对象,它的任务就是把相关资源分组。每个进程都有一个地址空间,如图2.10所示。
图2.10 进程的地址空间
这个所谓的进程图像必须在初始化CreateProcess
时加载至物理内存中。所有的资源(如文件句柄、子进程的信息、信号处理器等)都被储存起来。把它们以进程的形式分组在一起,更容易管理。
除进程外,还有一个重要的概念是线程。线程是CPU可执行调度的最小单位。也就是说,进程本身不能获得CPU时间,只有它的线程才可以。线程通过它的工作变量和栈来储存CPU寄存器的信息。栈包含与函数调用相关的数据,在每个函数被调用但尚未返回时,为其创建一个框架。线程可以在CPU上执行,而进程则不行。但是,进程至少必须有一个线程,通常把这个线程称为主线程。因此,当我们说在CPU上执行的进程时,指的是进程中的主线程。
进程用于分组资源,线程是在CPU上调度执行的实体。在同一个进程环境中可以执行多个线程,理解这点很重要。多线程并行运行在一个进程上下文,与在一个计算机中并行运行的多个进程相同。术语“多线程”指的是在单进程上下文中运行的多线程。
如图2.11所示,有3个进程,每个进程中都有一个线程。
图2.11 3个进程中各有1个线程
图2.12演示了一个有3个线程的进程。虽然这两种情况中都有3个线程,但是在图2.11中,每个线程都在不同的地址空间中运行,而图2.12中的3个线程共享同一个地址空间。
图2.12 有3个线程的进程
在单核CPU系统中运行多线程的进程时,各线程轮流运行。系统通过快速切换多个进程,营造并行处理的假象。多线程也以这样的方式运行。一个有3个线程的进程,其各线程表现为并行运行。单核CPU每次运行一个线程,花费CUP调度处理该进程时间的1/3(大概是这样,CPU时间取决于操作系统、调度算法等)。在多处理器系统中,情况类似。只有单核CPU执行线程时才与本书描述的方式相同。多核的好处是,可以并行运行更多的线程,充分发挥本地硬件的并行处理能力和多线程的执行能力。
下面的例子用两个线程实现一个简单的数组排序,演示了线程的基本用法。
确定安装并运行了Visual Studio。
1.创建一个新的默认Win32控制台应用程序,名为MultithreadedArraySort
。
2.打开MultithreadedArraySort.cpp
,并输入下面的代码:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <tchar.h>
using namespace std;
#define THREADS_NUMBER 2
#define ELEMENTS_NUMBER 200
#define BLOCK_SIZE ELEMENTS_NUMBER / THREADS_NUMBER
#define MAX_VALUE 1000
typedef struct _tagARRAYOBJECT
{
int* iArray;
int iSize;
int iThreadID;
} ARRAYOBJECT, *PARRAYOBJECT;
DWORD WINAPI ThreadStart(LPVOID lpParameter);
void PrintArray(int* iArray, int iSize);
void MergeArrays(int* leftArray, int leftArrayLenght, int*
rightArray, int rightArrayLenght, int* mergedArray);
int _tmain(int argc, TCHAR* argv[])
{
int iArray1[BLOCK_SIZE];
int iArray2[BLOCK_SIZE];
int iArray[ELEMENTS_NUMBER];
for (int iIndex = 0; iIndex < BLOCK_SIZE; iIndex++)
{
iArray1[iIndex] = rand() % MAX_VALUE;
iArray2[iIndex] = rand() % MAX_VALUE;
}
HANDLE hThreads[THREADS_NUMBER];
ARRAYOBJECT pObject1 = { &(iArray1[0]), BLOCK_SIZE, 0 };
hThreads[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)
ThreadStart, (LPVOID)&pObject1, 0, NULL);
ARRAYOBJECT pObject2 = { &(iArray2[0]), BLOCK_SIZE, 1 };
hThreads[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)
ThreadStart, (LPVOID)&pObject2, 0, NULL);
cout << "Waiting execution..." << endl;
WaitForMultipleObjects(THREADS_NUMBER, hThreads, TRUE, INFINITE);
MergeArrays(&iArray1[0], BLOCK_SIZE, &iArray2[0], BLOCK_SIZE, &iArray[0]);
PrintArray(iArray, ELEMENTS_NUMBER);
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
cout << "Array sorted..." << endl;
return 0;
}
DWORD WINAPI ThreadStart(LPVOID lpParameter)
{
PARRAYOBJECT pObject = (PARRAYOBJECT)lpParameter;
int iTmp = 0;
for (int iIndex = 0; iIndex < pObject->iSize; iIndex++)
{
for (int iEndIndex = pObject->iSize - 1; iEndIndex > iIndex; iEndIndex--)
{
if (pObject->iArray[iEndIndex] < pObject->iArray[iIndex])
{
iTmp = pObject->iArray[iEndIndex];
pObject->iArray[iEndIndex] = pObject->iArray[iIndex];
pObject->iArray[iIndex] = iTmp;
}
}
}
return 0;
}
void PrintArray(int* iArray, int iSize)
{
for (int iIndex = 0; iIndex < iSize; iIndex++)
{
cout << " " << iArray[iIndex];
}
cout << endl;
}
void MergeArrays(int* leftArray, int leftArrayLenght, int*
rightArray, int rightArrayLenght, int* mergedArray)
{
int i = 0;
int j = 0;
int k = 0;
while (i < leftArrayLenght && j < rightArrayLenght)
{
if (leftArray[i] < rightArray[j])
{
mergedArray[k] = leftArray[i];
i++;
}
else
{
mergedArray[k] = rightArray[j];
j++;
}
k++;
}
if (i >= leftArrayLenght)
{
while (j < rightArrayLenght)
{
mergedArray[k] = rightArray[j];
j++;
k++;
}
}
if (j >= rightArrayLenght)
{
while (i < leftArrayLenght)
{
mergedArray[k] = leftArray[i];
i++;
k++;
}
}
}
这个程序示例很简单,演示了线程的基本用法。该示例背后的思想是,为了节省执行时间而添加并行,把问题划分为几个小问题,并分配给几个线程(分而治之)。我们在前面提到过,把问题划分成若干更小的单元,更容易在实现中创建并行逻辑。同时,在并行中使用系统资源能优化应用程序并提高其运行速度。
如前所述,每个应用程序都有一个主线程。使用CreateThread
Win32 API创建其他线程:
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter, DWORD dwFlags, LPDWORD lpThreadId );
设置线程的开始地址(lpStartAddress
)和设置传给线程例程的值(lpParameter
)很重要。lpParameter
是一个预定义例程(函数)指针,如下代码所示:
typedef DWORD ( WINAPI *PTHREAD_START_ROUTINE )( LPVOID lpThreadParameter );
我们的ThreadStart
方法与指定的原型匹配,这也是开始执行线程的地方。CreateThread
API的第4个参数是一个要传递给线程例程的指针。如果要传递更多参数,可以创建一个结构或类,然后传递相应对象的地址。欲详细了解CreateThread
API,请查阅MSDN(http://msdn.microsoft.com/en-us/library/windows/desktop/ms682453%28v=vs.85%29.aspx)。
现在大部分应用程序都使用一些数据库。在许多情况下,这种应用程序通常会运行在不同的PC中,并同时进行读写操作。下面例子中的线程使用了MySQL数据库。
该示例要求安装MySQL C Connector,详情请查阅附录。成功安装MySQL C Connector后,运行运行Visual Studio。
1. 创建一个新的默认C++控制台应用程序,命名为MultithreadedDBTest
。
2.打开【解决方案资源管理器】,添加一个新的头文件,命名为CMYSQL.h
。打开CMYSQL.h
,并输入下面的代码:
#include "stdafx.h"
#include <stdio.h>
#include <stdlib.h>
#include <mysql.h>
class CMySQL
{
public:
static CMySQL* CreateInstance(char* szHostName, char* szDatabase,
char* szUserId, char* szPassword);
static void ReleaseInstance();
bool ConnectInstance();
bool DisconnectInstance();
bool ReadData(char* szQuery, char* szResult, size_t uBufferLenght);
bool WriteData(char* szQuery, char* szResult, size_t uBufferLenght);
private:
CMySQL(char* szHostName, char* szDatabase, char* szUserId, char*
szPassword);
~CMySQL();
char* szHostName;
char* szDatabase;
char* szUserId;
char* szPassword;
MYSQL* mysqlConnection;
static CMySQL* mySqlInstance;
};
3.打开【解决方案资源管理器】,添加一个新的CMYSQL.cpp
文件。打开CMYSQL.cpp
,并输入下面的代码:
#include "stdafx.h"
#include "CMySQL.h"
CMySQL* CMySQL::mySqlInstance = NULL;
CMySQL* CMySQL::CreateInstance(char* szHostName, char* szDatabase,
char* szUserId, char* szPassword)
{
if (mySqlInstance)
{
return mySqlInstance;
}
return new CMySQL(szHostName, szDatabase, szUserId, szPassword);
}
void CMySQL::ReleaseInstance()
{
if (mySqlInstance)
{
delete mySqlInstance;
}
}
CMySQL::CMySQL(char* szHostName, char* szDatabase, char* szUserId,
char* szPassword)
{
size_t length = 0;
this->szHostName = new char[length = strlen(szHostName) + 1];
strcpy_s(this->szHostName, length, szHostName);
this->szDatabase = new char[length = strlen(szDatabase) + 1];
strcpy_s(this->szDatabase, length, szDatabase);
this->szUserId = new char[length = strlen(szUserId) + 1];
strcpy_s(this->szUserId, length, szUserId);
this->szPassword = new char[length = strlen(szPassword) + 1];
strcpy_s(this->szPassword, length, szPassword);
}
CMySQL::~CMySQL()
{
delete szHostName;
delete szDatabase;
delete szUserId;
delete szPassword;
}
bool CMySQL::ConnectInstance()
{
MYSQL* mysqlLink = NULL;
try
{
mysqlConnection = mysql_init(NULL);
mysqlLink = mysql_real_connect(mysqlConnection, szHostName,
szUserId, szPassword, szDatabase, 3306, NULL, 0);
}
catch (...)
{
mysqlConnection = 0;
return false;
}
return mysqlLink ? true : false;
}
bool CMySQL::DisconnectInstance()
{
try
{
mysql_close(mysqlConnection);
return true;
}
catch (...)
{
return false;
}
}
bool CMySQL::ReadData(char* szQuery, char* szResult, size_t uBufferLenght)
{
int mysqlStatus = 0;
MYSQL_RES* mysqlResult = NULL;
MYSQL_ROW mysqlRow = NULL;
my_ulonglong numRows = 0;
unsigned numFields = 0;
try
{
mysqlStatus = mysql_query(mysqlConnection, szQuery);
if (mysqlStatus)
{
return false;
}
else
{
mysqlResult = mysql_store_result(mysqlConnection);
}
if (mysqlResult)
{
numRows = mysql_num_rows(mysqlResult);
numFields = mysql_num_fields(mysqlResult);
}
mysqlRow = mysql_fetch_row(mysqlResult);
if (mysqlRow)
{
if (!mysqlRow[0])
{
mysql_free_result(mysqlResult);
return false;
}
}
else
{
mysql_free_result(mysqlResult);
return false;
}
size_t szResultLength = strlen(mysqlRow[0]) + 1;
strcpy_s(szResult, szResultLength > uBufferLenght ?
uBufferLenght : szResultLength, mysqlRow[0]);
if (mysqlResult)
{
mysql_free_result(mysqlResult);
mysqlResult = NULL;
}
}
catch (...)
{
return false;
}
return true;
}
bool CMySQL::WriteData(char* szQuery, char* szResult, size_t
uBufferLenght)
{
try
{
int mysqlStatus = mysql_query(mysqlConnection, szQuery);
if (mysqlStatus)
{
size_t szResultLength = strlen("Failed!") + 1;
strcpy_s(szResult, szResultLength > uBufferLenght ?
uBufferLenght : szResultLength, "Failed!");
return false;
}
}
catch (...)
{
size_t szResultLength = strlen("Exception!") + 1;
strcpy_s(szResult, szResultLength > uBufferLenght ?
uBufferLenght : szResultLength, "Exception!");
return false;
}
size_t szResultLength = strlen("Success") + 1;
strcpy_s(szResult, szResultLength > uBufferLenght ?
uBufferLenght : szResultLength, "Success");
return true;
}
4.打开MultithreadedDBTest.cpp
,并添加下面的代码:
#include "stdafx.h"
#include "CMySQL.h"
#define BLOCK_SIZE 4096
#define THREADS_NUMBER 3
typedef struct
{
char szQuery[BLOCK_SIZE];
char szResult[BLOCK_SIZE];
bool bIsRead;
} QUERYDATA, *PQUERYDATA;
CRITICAL_SECTION cs;
CMySQL* mySqlInstance = NULL;
DWORD WINAPI StartAddress(LPVOID lpParameter)
{
PQUERYDATA pQueryData = (PQUERYDATA)lpParameter;
EnterCriticalSection(&cs);
if (mySqlInstance->ConnectInstance())
{
if (pQueryData->bIsRead)
{
memset(pQueryData->szResult, 0, BLOCK_SIZE - 1);
mySqlInstance->ReadData(pQueryData->szQuery,
pQueryData->szResult, BLOCK_SIZE - 1);
}
else
{
mySqlInstance->WriteData(pQueryData->szQuery,
pQueryData->szResult, BLOCK_SIZE - 1);
}
mySqlInstance->DisconnectInstance();
}
LeaveCriticalSection(&cs);
return 0L;
}
int main()
{
InitializeCriticalSection(&cs);
mySqlInstance = CMySQL::CreateInstance(
"mysql.services.expert.its.me", "expertit_9790OS",
"expertit_9790", "$dbpass_1342#");
if (mySqlInstance)
{
HANDLE hThreads[THREADS_NUMBER];
QUERYDATA queryData[THREADS_NUMBER] =
{
{ "select address from clients where id = 3;",
"", true },
{ "update clients set name='Merrill & Lynch' where id=2;",
"", false },
{ "select name from clients where id = 2;",
"", true }
};
for (int iIndex = 0; iIndex < THREADS_NUMBER; iIndex++)
{
hThreads[iIndex] = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)StartAddress,
&queryData[iIndex], 0, 0);
}
WaitForMultipleObjects(THREADS_NUMBER, hThreads, TRUE, INFINITE);
for (int iIndex = 0; iIndex < THREADS_NUMBER; iIndex++)
{
printf_s("%s\n", queryData[iIndex].szResult);
}
CMySQL::ReleaseInstance();
}
DeleteCriticalSection(&cs);
return system("pause");
}
上面的示例演示了在操作MySQL数据库的应用程序中实现线程同步。该例只使用了一种方法完成同步,另一种使用线程同步的方法是,通过数据库管理系统(Database Management System,DBMS)本身的机制,使用表锁(table lock)。这里的重点是,两个线程不能在同一时间内同时执行读或写操作,一个线程执行了读/
写操作,另一个线程就不能这样做。我们假设3个线程中有2个线程必须进行读操作,而第3个线程必须写入某些数据。还可以假设线程在读取之后,必须给主应用程序或某些其他处理程序发读取数据的信号,然后继续处理。写入数据的线程也是这样,从其他处理程序获得数据后要执行必要的操作(发信号)。本例中我们为了同步线程,使用了一个临界区对象。临界区对象将在第3章详细介绍。
上面提到,在操作MySQL数据库时还有一种更方便的方法同步线程。对于现代的MySQL DBMS,可以用表锁来完成。这里只是给读者提供一个思路,我们将演示一种方法,两个线程执行查询的同时还保持同步的读写操作,互不干扰。
1.读线程:
表锁TABLE_NAME
读;
在TABLE_NAME
中选择;
解除表锁。
2.写线程:
表锁TABLE_NAME
读;
在TABLE_NAME
中插入值(…);
更新TABLE_NAME
设置;
解除表锁。
3.单线程中的操作——其他线程必须等待:
表锁TABLE_NAME TABLE_ALIAS
读,TABLE_NAME
写;
在TABLE_NAME
中选择一些内容作为TABLE_ALIAS
;
在TABLE_NAME
中插入值…;
解除表锁。
注意,MySQL有多种引擎(InnoDB、MyISAM、Memory等),不同的引擎其表锁和锁粒度都不同。
既可以在用户空间也可以在内核中实现线程包。具体选择在哪里实现还存在一些争议,在一些实现中可能会混合使用内核线程和用户线程。
我们将讨论在不同地方实现线程包的方法及优缺点。第1种方法是,把整个线程包放进用户空间,内核完全不知道。就内核而言,它管理着普通的单线程进程。这种方法的优点和最显著的优势是,可以在不支持线程的操作系统中实现用户级线程包。
过去,传统的操作系统就采用这种方法,甚至沿用至今。用这种方法,线程可以通过库来实现。所有这些实现都具有相同的通用结构。线程运行在运行时系统的顶部,该系统是专门管理线程的过程集合。我们在前面见过一些例子(CreateThread
、TerminateThread
等),以后还会见到更多。
下面的程序示例演示了在用户空间中的线程用法。我们要复制大型文件,但是不想一开始就读取整个文件的内容,或者更优化地一部分一部分地读取,而且不用在文件中写入数据。这就涉及2.5节中提到的生产者-消费者问题。
确定安装并运行了Visual Studio。
1. 创建一个新的Win32应用程序项目,并命名为ConcurrentFileCopy
。
2. 打开【解决方案资源管理器】,添加一个新的头文件,命名为ConcurrentFileCopy.h
。打开ConcurrentFileCopy.h
,并输入下面的代码:
#pragma once
#include <windows.h>
#include <commctrl.h>
#include <memory.h>
#include <tchar.h>
#include <math.h>
#pragma comment ( lib, "comctl32.lib" )
#pragma comment ( linker, "\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' \
version='6.0.0.0' processorArchitecture='*' \
publicKeyToken='6595b64144ccf1df' language='*'\"" )
ATOM RegisterWndClass(HINSTANCE hInstance);
HWND InitializeInstance(HINSTANCE hInstance, int nCmdShow, HWND& hWndPB);
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
DWORD WINAPI ReadRoutine(LPVOID lpParameter);
DWORD WINAPI WriteRoutine(LPVOID lpParameter);
BOOL FileDialog(HWND hWnd, LPTSTR szFileName, DWORD
dwFileOperation);
DWORD GetBlockSize(DWORD dwFileSize);
#define BUTTON_CLOSE 100
#define FILE_SAVE 0x0001
#define FILE_OPEN 0x0002
#define MUTEX_NAME _T("__RW_MUTEX__")
typedef struct _tagCOPYDETAILS
{
HINSTANCE hInstance;
HWND hWndPB;
LPTSTR szReadFileName;
LPTSTR szWriteFileName;
} COPYDETAILS, *PCOPYDETAILS;
3.现在,打开【解决方案资源管理器】,并添加一个新的源文件,命名为ConcurrentFileCopy.cpp
。打开ConcurrentFileCopy.c
,并输入下面的代码:
#include "ConcurrentFileCopy.h"
TCHAR* szTitle = _T("Concurrent file copy");
TCHAR* szWindowClass = _T("_ _CFC_WND_CLASS_ _");
DWORD dwReadBytes = 0;
DWORD dwWriteBytes = 0;
DWORD dwBlockSize = 0;
DWORD dwFileSize = 0;
HLOCAL pMemory = NULL;
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPTSTR
szCmdLine, int iCmdShow)
{
UNREFERENCED_PARAMETER(hPrev);
UNREFERENCED_PARAMETER(szCmdLine);
RegisterWndClass(hInstance);
HWND hWnd = NULL;
HWND hWndPB = NULL;
if (!(hWnd = InitializeInstance(hInstance, iCmdShow, hWndPB)))
{
return 1;
}
MSG msg = { 0 };
TCHAR szReadFile[MAX_PATH];
TCHAR szWriteFile[MAX_PATH];
if (FileDialog(hWnd, szReadFile, FILE_OPEN) && FileDialog(hWnd,
szWriteFile, FILE_SAVE))
{
COPYDETAILS copyDetails = { hInstance, hWndPB, szReadFile,
szWriteFile };
HANDLE hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME);
HANDLE hReadThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)ReadRoutine, ©Details, 0, NULL);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
CloseHandle(hReadThread);
CloseHandle(hMutex);
}
else
{
MessageBox(hWnd, _T("Cannot open file!"),
_T("Error!"), MB_OK);
}
LocalFree(pMemory);
UnregisterClass(szWindowClass, hInstance);
return (int)msg.wParam;
}
ATOM RegisterWndClass(HINSTANCE hInstance)
{
WNDCLASSEX wndEx;
wndEx.cbSize = sizeof(WNDCLASSEX);
wndEx.style = CS_HREDRAW | CS_VREDRAW;
wndEx.lpfnWndProc = WndProc;
wndEx.cbClsExtra = 0;
wndEx.cbWndExtra = 0;
wndEx.hInstance = hInstance;
wndEx.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wndEx.hCursor = LoadCursor(NULL, IDC_ARROW);
wndEx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wndEx.lpszMenuName = NULL;
wndEx.lpszClassName = szWindowClass;
wndEx.hIconSm = LoadIcon(wndEx.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
return RegisterClassEx(&wndEx);
}
HWND InitializeInstance(HINSTANCE hInstance, int iCmdShow, HWND& hWndPB)
{
HWND hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPED
| WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, 200, 200, 440, 290,
NULL, NULL, hInstance, NULL);
RECT rcClient = { 0 };
int cyVScroll = 0;
if (!hWnd)
{
return NULL;
}
HFONT hFont = CreateFont(14, 0, 0, 0, FW_NORMAL, FALSE, FALSE,
FALSE, BALTIC_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN,
_T("Microsoft Sans Serif"));
HWND hButton = CreateWindow(_T("BUTTON"), _T("Close"), WS_CHILD
| WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP, 310, 200, 100, 25,
hWnd, (HMENU)BUTTON_CLOSE, hInstance, NULL);
SendMessage(hButton, WM_SETFONT, (WPARAM)hFont, TRUE);
GetClientRect(hWnd, &rcClient);
cyVScroll = GetSystemMetrics(SM_CYVSCROLL);
hWndPB = CreateWindow(PROGRESS_CLASS, (LPTSTR)NULL, WS_CHILD |
WS_VISIBLE, rcClient.left, rcClient.bottom - cyVScroll,
rcClient.right, cyVScroll, hWnd, (HMENU)0, hInstance, NULL);
SendMessage(hWndPB, PBM_SETSTEP, (WPARAM)1, 0);
ShowWindow(hWnd, iCmdShow);
UpdateWindow(hWnd);
return hWnd;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_COMMAND:
{
switch (LOWORD(wParam))
{
case BUTTON_CLOSE:
{
DestroyWindow(hWnd);
break;
}
}
break;
}
case WM_DESTROY:
{
PostQuitMessage(0);
break;
}
default:
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return 0;
}
DWORD WINAPI ReadRoutine(LPVOID lpParameter)
{
PCOPYDETAILS pCopyDetails = (PCOPYDETAILS)lpParameter;
HANDLE hFile = CreateFile(pCopyDetails->szReadFileName,
GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == (HANDLE)INVALID_HANDLE_VALUE)
{
return FALSE;
}
dwFileSize = GetFileSize(hFile, NULL);
dwBlockSize = GetBlockSize(dwFileSize);
HANDLE hWriteThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)WriteRoutine, pCopyDetails, 0, NULL);
size_t uBufferLength = (size_t)ceil((double) dwFileSize / (double)dwBlockSize);
SendMessage(pCopyDetails->hWndPB, PBM_SETRANGE, 0,
MAKELPARAM(0, uBufferLength));
pMemory = LocalAlloc(LPTR, dwFileSize);
void* pBuffer = LocalAlloc(LPTR, dwBlockSize);
int iOffset = 0;
DWORD dwBytesRed = 0;
do
{
ReadFile(hFile, pBuffer, dwBlockSize, &dwBytesRed, NULL);
if (!dwBytesRed)
{
break;
}
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE,
MUTEX_NAME);
WaitForSingleObject(hMutex, INFINITE);
memcpy((char*)pMemory + iOffset, pBuffer, dwBytesRed);
dwReadBytes += dwBytesRed;
ReleaseMutex(hMutex);
iOffset += (int)dwBlockSize;
} while (true);
LocalFree(pBuffer);
CloseHandle(hFile);
CloseHandle(hWriteThread);
return 0;
}
DWORD WINAPI WriteRoutine(LPVOID lpParameter)
{
PCOPYDETAILS pCopyDetails = (PCOPYDETAILS)lpParameter;
HANDLE hFile = CreateFile(pCopyDetails->szWriteFileName,
GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == (HANDLE)INVALID_HANDLE_VALUE)
{
return FALSE;
}
DWORD dwBytesWritten = 0;
int iOffset = 0;
do
{
int iRemainingBytes = (int)dwFileSize - iOffset;
if (iRemainingBytes <= 0)
{
break;
}
Sleep(10);
if (dwWriteBytes < dwReadBytes)
{
DWORD dwBytesToWrite = dwBlockSize;
if (!(dwFileSize / dwBlockSize))
{
dwBytesToWrite = (DWORD)iRemainingBytes;
}
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, MUTEX_NAME);
WaitForSingleObject(hMutex, INFINITE);
WriteFile(hFile, (char*)pMemory + iOffset, dwBytesToWrite,
&dwBytesWritten, NULL);
dwWriteBytes += dwBytesWritten;
ReleaseMutex(hMutex);
SendMessage(pCopyDetails->hWndPB, PBM_STEPIT, 0, 0);
iOffset += (int)dwBlockSize;
}
} while (true);
CloseHandle(hFile);
return 0;
}
BOOL FileDialog(HWND hWnd, LPTSTR szFileName, DWORD dwFileOperation)
{
#ifdef _UNICODE
OPENFILENAMEW ofn;
#else
OPENFILENAMEA ofn;
#endif
TCHAR szFile[MAX_PATH];
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hWnd;
ofn.lpstrFile = szFile;
ofn.lpstrFile[0] = '\0';
ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = _T("All\0*.*\0Text\0*.TXT\0");
ofn.nFilterIndex = 1;
ofn.lpstrFileTitle = NULL;
ofn.nMaxFileTitle = 0;
ofn.lpstrInitialDir = NULL;
ofn.Flags = dwFileOperation == FILE_OPEN ? OFN_PATHMUSTEXIST |
OFN_FILEMUSTEXIST : OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
if (dwFileOperation == FILE_OPEN)
{
if (GetOpenFileName(&ofn) == TRUE)
{
_tcscpy_s(szFileName, MAX_PATH - 1, szFile);
return TRUE;
}
}
else
{
if (GetSaveFileName(&ofn) == TRUE)
{
_tcscpy_s(szFileName, MAX_PATH - 1, szFile);
return TRUE;
}
}
return FALSE;
}
DWORD GetBlockSize(DWORD dwFileSize)
{
return dwFileSize > 4096 ? 4096 : 512;
}
我们创建了一个和哲学家就餐示例非常像的UI。例程MyRegisterClass
、InitInstance
和WndProc
几乎都一样。我们在程序中添加FileDialog
来询问用户读写文件的路径。为了读和写,分别启动了两个线程。
操作系统的调度十分复杂。我们根本不知道是调度算法还是硬件中断使得某线程被调度在CUP中执行。这意味着写线程可能在读线程之前执行。出现这种情况会导致一个异常,因为写线程没东西可写。
因此,我们在写操作中添加了if
条件,如下代码所示:
if ( dwBytesWritten < dwBytesRead )
{
WriteFile(hFile, pCharTmp, sizeof(TCHAR) * BLOCK_SIZE, &dwFileSize, NULL);
dwBytesWritten += dwFileSize;
SendMessage( hProgress, PBM_STEPIT, 0, 0 );
}
线程在获得互斥量后,才能执行写操作。尽管如此,系统仍然有可能在读线程之前调度写线程,此时缓冲区是空的。因此,每次读线程获得一些内容,就要把读取的字节数加给dwBytesRed
变量,只有写线程的字节数小于读线程的字节数,才可以执行写操作。否则,本轮循环将跳过写操作,并释放互斥量供其他线程使用。
生产者-消费者问题也称为有界缓冲问题。两个进程共享一个固定大小的公共缓冲区。生产者把信息放入缓冲区,消费者把信息从缓冲区中取出来。该问题也可扩展为m个生产者和n个消费者的问题。不过,这里我们简化了问题,只考虑一个生产者和一个消费者。当生产者往缓冲区放入新项目时缓冲区满了,就会产生问题。解决的方案是,让生产者睡眠,并在消费者已经移除一个或多个项时唤醒生产者。同理,如果消费者要从缓冲区取项目时缓冲区为空,也会产生问题。解决的方案是,让消费者睡眠,等生产者把项目放入缓冲区后再唤醒它。这种方法看起来很简单,但是会导致竞态条件。读者可以用学过的知识,尝试解决类似的情况。
整个内核就是一个进程,许多系统(内核)线程在其上下文中运行。内核有一个线程表,跟踪该系统中所有的线程。
内核维护这个传统的进程表以跟踪进程。那些可以阻塞线程的函数调用可作为系统调用执行,这比执行系统过程的代价更高。当线程被阻塞时,内核必须运行其他线程。当线程被毁坏时,则被标记为不可运行。但是,它的内核数据结构不会受到影响。然后在创建新的线程时,旧的线程将被再次激活,回收资源以备后用。当然,也可以回收用户级线程,但如果线程管理开销非常小,就没必要这样做。
下面的示例要求安装WinDDK(Driver Development Kit,驱动程序开发工具包),详情请参阅附录。成功安装WinDDK后,运行Visual Studio。
1. 创建一个新的Win32应用程序项目,并命名为KernelThread
。
2. 打开【解决方案资源管理器】,并添加一个新的头文件,命名为ThreadApp.h
。打开ThreadApp.h
,并输入下面的代码:
#include <windows.h>
#include <tchar.h>
#define DRIVER_NAME TEXT( "TestDriver.sys" )
#define DRIVER_SERVICE_NAME TEXT( "TestDriver" )
#define Message(n) MessageBox(0, TEXT(n), \
TEXT("Test Driver Info"), 0)
BOOL StartDriver(LPTSTR szCurrentDriver);
BOOL StopDriver(void);
3.现在,打开【解决方案资源管理器】,并添加一个新的源文件,命名为ThreadApp.cpp
。打开ThreadApp.cpp
,并输入下面的内容:
#include "ThreadApp.h"
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR szCommandLine, int iCmdShow)
{
StartDriver(DRIVER_NAME);
ShellAbout(0, DRIVER_SERVICE_NAME, TEXT(""), NULL);
StopDriver();
return 0;
}
BOOL StartDriver(LPTSTR szCurrentDriver)
{
HANDLE hFile = 0;
DWORD dwReturn = 0;
SC_HANDLE hSCManager = { 0 };
SC_HANDLE hService = { 0 };
SERVICE_STATUS ServiceStatus = { 0 };
TCHAR szDriverPath[MAX_PATH] = { 0 };
GetSystemDirectory(szDriverPath, MAX_PATH);
TCHAR szDriver[MAX_PATH + 1];
#ifdef _UNICODE
wsprintf(szDriver, L"\\drivers\\%ws", DRIVER_NAME);
#else
sprintf(szDriver, "\\drivers\\%s", DRIVER_NAME);
#endif
_tcscat_s(szDriverPath, (_tcslen(szDriver) + 1) * sizeof(TCHAR),
szDriver);
BOOL bSuccess = CopyFile(szCurrentDriver, szDriverPath, FALSE);
if (bSuccess == FALSE)
{
Message("copy driver failed");
return bSuccess;
}
hSCManager = OpenSCManager(NULL, NULL,
SC_MANAGER_CREATE_SERVICE);
if (hSCManager == 0)
{
Message("open sc manager failed!");
return FALSE;
}
hService = CreateService(hSCManager, DRIVER_SERVICE_NAME,
DRIVER_SERVICE_NAME, SERVICE_START | DELETE | SERVICE_STOP,
SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
szDriverPath, NULL, NULL, NULL, NULL, NULL);
if (hService == 0)
{
hService = OpenService(hSCManager, DRIVER_SERVICE_NAME,
SERVICE_START | DELETE | SERVICE_STOP);
Message("create service failed!");
}
if (hService == 0)
{
Message("open service failed!");
return FALSE;
}
BOOL startSuccess = StartService(hService, 0, NULL);
if (startSuccess == FALSE)
{
Message("start service failed!");
return startSuccess;
}
CloseHandle(hFile);
return TRUE;
}
BOOL StopDriver(void)
{
SC_HANDLE hSCManager = { 0 };
SC_HANDLE hService = { 0 };
SERVICE_STATUS ServiceStatus = { 0 };
TCHAR szDriverPath[MAX_PATH] = { 0 };
GetSystemDirectory(szDriverPath, MAX_PATH);
TCHAR szDriver[MAX_PATH + 1];
#ifdef _UNICODE
wsprintf(szDriver, L"\\drivers\\%ws", DRIVER_NAME);
#else
sprintf(szDriver, "\\drivers\\%s", DRIVER_NAME);
#endif
_tcscat_s(szDriverPath, (_tcslen(szDriver) + 1) * sizeof(TCHAR),
szDriver);
hSCManager = OpenSCManager(NULL, NULL,
SC_MANAGER_CREATE_SERVICE);
if (hSCManager == 0)
{
return FALSE;
}
hService = OpenService(hSCManager, DRIVER_SERVICE_NAME,
SERVICE_START | DELETE | SERVICE_STOP);
if (hService)
{
ControlService(hService, SERVICE_CONTROL_STOP,
&ServiceStatus);
DeleteService(hService);
CloseServiceHandle(hService);
BOOL ifSuccess = DeleteFile(szDriverPath);
return TRUE;
}
return FALSE;
}
4.现在,打开【解决方案资源管理器】,创建一个新的空Win32控制台项目,并命名为DriverApp
。
5.添加一个新的头文件,命名为DriverApp.h
,并输入以下代码:
#include <ntddk.h>
DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD OnUnload;
6.打开【解决方案资源管理器】,在DriverApp
项目下,添加一个新的源文件,命名为DriverApp.cpp
。打开DriverApp.cpp
,并输入以下代码:
#include "DriverApp.h"
VOID ThreadStart(PVOID lpStartContext)
{
PKEVENT pEvent = (PKEVENT)lpStartContext;
DbgPrint("Hello! I am kernel thread. My ID is %u. Regards..",
(ULONG)PsGetCurrentThreadId());
KeSetEvent(pEvent, 0, 0);
PsTerminateSystemThread(STATUS_SUCCESS);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT theDriverObject, PUNICODE_STRING
theRegistryPath)
{
HANDLE hThread = NULL;
NTSTATUS ntStatus = 0;
OBJECT_ATTRIBUTES ThreadAttributes;
KEVENT kEvent = { 0 };
PETHREAD pThread = 0;
theDriverObject->DriverUnload = OnUnload;
DbgPrint("Entering KERNEL mode..");
InitializeObjectAttributes(&ThreadAttributes, NULL, OBJ_KERNEL_HANDLE,
NULL, NULL);
__try
{
KeInitializeEvent(&kEvent, SynchronizationEvent, 0);
ntStatus = PsCreateSystemThread(&hThread, GENERIC_ALL,
&ThreadAttributes, NULL, NULL, (PKSTART_ROUTINE)&ThreadStart,
&kEvent);
if (NT_SUCCESS(ntStatus))
{
KeWaitForSingleObject(&kEvent, Executive, KernelMode, FALSE,
NULL);
ZwClose(hThread);
}
else
{
DbgPrint("Could not create system thread!");
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
DbgPrint("Error while creating system thread!");
}
return STATUS_SUCCESS;
}
VOID OnUnload(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Leaving KERNEL mode..");
}
首先,创建了一个Win32应用程序,仅作为演示用。我们只想把驱动程序加载至内核中,还没有UI,甚至没有消息循环。然后,程序将显示ShellAbout
对话框,这仅仅是为了让用户有时间阅读DbgView
输出(欲详细了解DbgView
,请参阅附录)。在用户关闭ShellAbout
对话框后,程序将卸载驱动程序,应用程序结束。
我们创建的Win32应用程序只能加载和卸载驱动程序,所以不做进一步解释了。现在,来看DriverApp
项目。为编译驱动程序设置好Visual Studio后(请查阅附录了解详细的Visual Studio的编译设置),我们声明了下面两个主例程,每个驱动程序都必须在DriverApp.h
头文件中:
DRIVER_INITIALIZE DriverEntry;
DRIVER_UNLOAD OnUnload;
这两个例程是驱动程序入口点和驱动程序的卸载例程。我们将使用驱动程序入口点初始化一个线程对象,并启动内核线程。新创建的线程将只写入一条显示它唯一标识符的消息,然后立刻返回。要说明的是,深入探讨和开发内核超出了本书讨论的范围,我们在这里浅尝辄止。因为驱动程序被编译为/TC2
,我们必须确保在执行第一条命令之前已经声明了所有变量。如下代码所示:
HANDLE hThread = NULL;
NTSTATUS ntStatus = 0;
OBJECT_ATTRIBUTES ThreadAttributes;
KEVENT kEvent = { 0 };
PETHREAD pThread = 0;
然后,还必须设置卸载例程:
theDriverObject->DriverUnload = OnUnload;
另外,在创建内核线程之前,要用InitializeObjectAttributes
例程初始化ThreadAttribute
对象:
InitializeObjectAttributes(&ThreadAttributes, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
内核开发必须执行得非常谨慎,哪怕是一丁点儿错误都会导致蓝屏死机(BSOD)或机器崩溃。为此,我们使用_ _try
- _ _except
块,它与我们熟悉的try
-catch
块稍有不同。
在内核中创建句柄和在用户空间中创建句柄不同。KeWaitForSingleObject
例程无法使用PsCreateSystemThread
返回的句柄。我们要在KeWaitForSingleObject
返回时在线程中初始化一个触发的事件(事件将在第3章中详细介绍)。PsCreateSystemThread
例程必须与ZwClose
例程成对调用。调用ZwClose
关闭内核句柄和防止内存泄漏。
最后,我们要实现PKSTART_ROUTINE
或线程的开始地址,线程的指令从这里开始执行。下面是一个示例:
VOID (__stdcall* KSTART_ROUTINE)( PVOID StartContext );
我们已经通过PsCreateSystemThread
的最后一个参数传递了一个指向KEVENT
的指针。现在,使用DbgPrint
把相应的线程ID写入消息供用户阅读。然后,设置一个事件,以便相应的KeWaitForSingleObject
调用可以返回并安全地退出驱动程序。确保PsTerminateSystemThread
没有返回。内核将在卸载驱动程序时清理线程对象。
虽然内核线程能解决一些问题,但也不是万能的。例如,当多线程进程创建其他多线程进程时会发生什么情况?应该创建与旧进程的线程一样多的新进程,还是创建只有一个线程的新进程?在多数情况下,这取决于你下一步打算用进程做什么。如果要启动一个新程序,也许应该创建只有一个线程的进程。但如果是继续执行,也许应该创建具有同样数量线程的进程才对。