Win32多线程编程(1) — 基础概念篇

摘要:
线程是在进程中执行代码的独立实体。线程不像进程那样按照严格的父子关系组织。当进程退出时,该进程生成的所有线程将被强制退出并清除。Win32多线程体系结构主线程可以在运行过程中创建一个新的辅助线程,称为多线程。进程中可以同时执行多个线程。为了使它们能够“同时”运行,操作系统依次为每个线程分配CPU时间片。
 

内核对象的基本概念

Windows系统是非开源的,它提供给我们的接口是用户模式的,即User-Mode API。当我们调用某个API时,需要从用户模式切换到内核模式的I/O System Services API。例如我们调用Kernel32.dll中的CreateFile创建文件,最终将执行ntdll.dll中的系统服务NtCreateFile。

内核为我们创建的文件对象以内核级数据结构FILE_OBJECT存储管理,内核级文件信息数据结构包括FILE_BASIC_INFORMATION、FILE_STANDARD_INFORMATION等,但是系统提供给我们在用户模式下与这个文件对象交互的接口是一个文件句柄(HANDLE)。

内核对象和普通的数据结构间的最大区别在于其内部数据结构是隐藏的,我们无法(系统没有操作接口)直接读或改变对象内部的数据结构。这里的文件句柄实际是文件内核对象在内核分配的内存中的一个索引,我们执行后期的用户模式下的文件操作调用都需传入文件句柄参数,内核根据该句柄来查找(定位)我们要操作的对象。类似的Windows提供的创建线程的API—CreateThread创建的内核对象也是以句柄(HANDLE)的形式返回给用户,后期对该线程内核对象的操作都需提供该句柄参数。

因为内核对象的所有者是内核,而不是进程,所以何时撤销内核对象由内核决定,而内核做这个决定的依据就是该内核对象是否仍然被使用。那么如何判断内核对象是否被使用呢?何时释放回收该内核对象资源呢?这就引出了内核对象的使用计数(Usage Count)问题。

内核对象是进程内的资源,使用计数属性指明进程对特定内核对象的引用次数,当系统发现引用次数是 0 时,它就会自动关闭资源。事实上这种机制是很简单的,一个进程在第一次创建内核对象的时候,系统为进程分配内核对象资源,并将该内核对象的使用计数属性初始化为1;以后每次打开这个内核对象,系统就会将使用计数加 1,如果关闭它,系统将使用计数减1,减到 0 就说明进程对这个内核对象的所有引用都已关闭,系统应该释放此内核对象资源。

我们编写程序都是在用户模式下进行的,要做内核级调试或者观察内部数据结构,需要下载系统符号(Symbols)。因为我们无法真正去操作其内部数据结构,又无内核源码,所以使用Windbg等工具一窥Windows系统内幕。结合具体内核对象的属性,通过观察其内部数据结构,感知其内核机制及操作流程。

 

进程和线程的基本概念

进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行过程,是系统进行资源分配和调度的独立单位。进程是由进程控制块(PCB)、程序段、数据段三部分组成。其中进程控制块是存放进程管理和控制信息的数据结构,是进程存在的唯一标志。

 “进程是一个正在运行的程序,它拥有自己的虚拟地址空间,拥有自己的代码、数据和其他系统资源,如进程创建的文件、管道、同步对象等。一个进程也包含了一个或者多个运行在此进程内的线程。”

进程是执行程序的实例。例如,当你运行记事本程序notepad.exe时,你就创建了一个用来容纳组成notepad.exe的代码及其所需调用动态链接库的进程。每个进程均运行在其专用且受保护的地址空间内。因此,如果你同时运行记事本的两个拷贝,该程序正在使用的数据在各自实例中是彼此独立的。在记事本的一个拷贝中将无法看到该程序的第二个实例打开的数据。

在以上语境中,存储在磁盘上的notepad.exe程序是一连串静态的指令,而进程则是一个容器,它包含了一系列运行在这个程序实例上下文中的线程使用的资源。进程是不活泼的。一个进程要完成任何事情,必须有一个运行在它的地址空间上的线程。此线程负责执行该进程地址空间的代码。每个进程至少拥有一个在它的地址空间中运行的线程。对一个不包含任何线程的进程来说,它是没有理由继续存在下去的,系统会自动地销毁此进程和它的地址空间。

线程是进程内执行代码的独立实体。线程(Thread)是进程内的一个独立执行单元,是CPU调度和分派的基本单位。一个线程就是运行在一个进程上下文中的一个逻辑流,它描述了进程内代码的执行路径。每个线程都有自己的线程上下文(Context),包括唯一的整数线程id,栈(Stack),栈指针(Stack Pointer),程序计数器(Program Counter),通用目的寄存器。所有运行在一个进程中的线程共享该进程的整个虚拟地址空间。

线程内核对象(Thread Kernel Object)和线程上下文(Thread Context)等概念,参考《线程的数据结构》。在WinDbg中,可通过lkd> dt nt!_kthread查看线程内核对象数据结构;通过lkd> dt nt!_teb查看线程TEB数据结构;通过lkd> dt nt!_context查看线程上下文数据结构。

抛开线程实体,进程中的程序代码是不可能执行的。操作系统创建进程后,会创建一个线程执行进程中的代码。通常我们把这个线程称为该进程的主线程,主线程在运行过程中可能会创建其他线程。一般将主线程创建的线程称为该进程的辅助线程

从从属关系的角度来讲,线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间。一个进程可以包含若干线程,线程可以帮助应用程序同时做几件事(比如一个线程向磁盘写入文件,另一个则接收用户的按键操作并及时做出反应,互相不干扰),在程序被运行后中,系统首先要做的就是为该程序进程建立一个默认线程,然后程序可以根据需要自行添加或删除相关的线程。

线程不像进程按照严格的父子关系来组织。和一个进程相关的线程组成一个对等线程池,一个线程可以杀死其任意对等线程。每个线程都能读写相同的共享数据。当进程退出时该进程所产生的线程都会被强制退出并清除。

Win32多线程架构

主线程在运行过程中可以创建新的辅助线程,即所谓的多线程。多线程较之多进程的优点在于,线程的上下文要比进程的上下文小的多,所以线程的上下文切换要比进程的上下文切换快得多。

进程的主线程的进入点为main函数,辅助线程的进入点为线程函数(Thread Procedure)。

进程中同时可以有多个线程在执行,为了使它们能够“同时”运行,操作系统为每个线程轮流分配 CPU时间片。为了充分地利用 CPU,提高软件产品的性能,一般情况下,Win32基于窗口的GUI应用程序使用主线程接受用户的输入,显示运行结果,而创建新的线程(称为辅助线程)来处理长时间的操作,比如读写文件、访问网络等。这样,即便是在程序忙于繁重的工作时也可以由专门的线程响应用户命令。

换句话说,程序的主线程是一个老板,而其它线程是老板的职员。老板将繁重的工作丢给职员处理,而他自己保持和外界的联系。因为那些线程仅仅是职员,所以它们不会举行记者招待会,它们会认真地完成分内职务,将结果报告给老板,并等待他们的下一个任务。而对外的一切事务谈判都交由老板等管理层交涉。

一个程序中的线程是同一程序的不同部分,因此他们共享程序的资源,如内存和打开的文件。因为线程共享程序的内存,所以他们还共享静态变量。然而,每个线程都有他们自己的堆栈,因此动态变量(自动变量)对每个线程是唯一的。每个线程还有各自的处理器状态(和数学协处理器状态),这个状态在进行线程切换期间被储存和恢复,也即所谓的上下文切换。

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。理论上,安装了N个CPU的PC,在某一时刻,系统底层所能并发执行的线程个数为N。对于单核PC,多线程微观串行,如果开辟的线程过多,则频繁的线程上下文切换将会耗费较多的CPU时钟周期。因此,多线程并不是多多益善,这便涉及到多线程的池化管理问题。

Win32线程消息队列

与基于MS - DOS的应用程序不同,Windows的应用程序是事件(消息)驱动的。它们不会显式地调用函数(如C运行时库调用)来获取输入,而是等待windows向它们传递输入。 windows系统把应用程序的输入事件传递给各个窗口,每个窗口有一个函数,称为窗口消息处理函数。窗口消息处理函数处理各种用户输入,处理完成后再将控制权交还给系统。窗口消息处理函数一般是在注册一个窗口的时候指定的。

在Windows NT和Windows 98中,没有消息队列线程和无消息队列线程的区别,每个线程在建立时都会有它自己的消息队列,可通过lkd> dt nt!_kthread查看其中的_KTHREAD::_KQUEUE*对象Queue。应用程序调用GetMessage/PeekMessage函数从调用线程消息队列中取出指定窗口(HWND)的消息,调用SendMessage/PostMessage函数向调用线程消息队列中压入指定窗口(HWND)的消息。

书籍参考:

《Windows 2000系统编程》

《Windows核心编程》

《Win32多线程程序设计》

《C++面向对象多线程编程》

专题参考:

Windows进程/线程浅谈

架构设计:进程还是线程?

C++多线程

VC多线程编程

从单线程到多线程

Windows多线程编程总结

深入浅出Win32多线程程序设计

Multithreaded Programming with ThreadMentor

Chrome源码剖析[1] - Chrome的多线程模型

Chrome源码剖析[2] - Chrome的进程间通信

Chrome源码剖析[3] - Chrome的进程模型

免责声明:文章转载自《Win32多线程编程(1) — 基础概念篇》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇WAP学习小总结svn安装配置下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

(转)乐观的并发策略——基于CAS的自旋

  悲观者与乐观者的做事方式完全不一样,悲观者的人生观是一件事情我必须要百分之百完全控制才会去做,否则就认为这件事情一定会出问题;而乐观者的人生观则相反,凡事不管最终结果如何,他都会先尝试去做,大不了最后不成功。这就是悲观锁与乐观锁的区别,悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据。这一节将对...

网络通信框架Apache MINA

Apache MINA(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用程序提供了非常便利的框架。当前发行的 MINA 版本支持基于 Java NIO 技术的TCP/UDP 应用程序开发、串口通讯程序。Mina 的应用层:一个...

前端worker之web worker

web worker 背景 众所周知javascript是单线程的,同一时间内只能做一件事情。这是十分必要的,设想,如果js是多线程的。有个dom元素两个线程同时做了改变,一个display:none,另一个display:block,这样让浏览器就无所适从了。出于此种考虑,单线程的js就这样一直延续下来,但是凡事必有两面性,虽然单线程保证了一些ui操作的...

Android开发——Android系统启动以及APK安装、启动过程

0. 前言   从Android手机打开开关,到我们可以使用其中的app时,这个启动过程到底是怎么样的?   1.  系统上电 当给Android系统上电,在电源接通的瞬间,CPU内的寄存器和各引脚均会被置为初始状态,CPU复位之后,程序指针会指向启动地址,从该地址读取并直接运行启动程序的可执行代码,或者将可执行代码与数据载入CPU内置的RAM中再运行。这...

jvm参数优化

一、HotSpot JVM 提供了三类参数 现在的JVM运行Java程序(和其它的兼容性语言)时在高效性和稳定性方面做的非常出色。例如:自适应内存管理、垃圾收集、及时编译、动态类加载、锁优化等。虽然有了这种程度的自动化(或者说有这么多自动化),但是JVM仍然提供了足够多的外部监控和手动调优工具(允许命令行参数可以在JVM启动时传入到JVM中)。在有错误或低...

Delphi中的线程类

文章来源: http://liukun966123.my.gsdn.net/2004/10/22/4797/ Delphi中的线程类 转贴于 华夏黑客同盟 http://www.77169.org Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉都有说到,但基本上都是对 TThread类的几个成员作一简单介绍,再...