线程组成两部分:
1. 一个线程的内核对象,操作系统用它管理线程。
2. 一个线程栈,用于维护线程执行时所需的所有函数参数和局部变量。
何时创建线程?举例:
操作系统的Windows Indexing Services,磁盘碎片整理程序等,都是使用多线程进行性能优化的例子。
线程内幕:
线程的上下文context对象 ,用于线程的调度,如下这张图详细的介绍,线程内核对象以及线程的地址空间,从而知道线程执行原理。
线程内核对象 线程栈
线程创建时,会先创建一个线程内核对象(分配在进程的地址空间上),如上图,存储上下文context(一个数据结构)及一些统计信息,具体包括:
1.寄存器SP:指向栈中线程函数指针的地址
2.寄存器IP:指向装载的NTDLL.dll里RtlUserThreadStart函数地址
3.Usage Count:引用计数,初始化为2
4.Suspend Count:挂起数,初始化为1。
5.ExitCode:退出代码,线程在运行时为STILL_ACTIVE(且初始化为该值)
6.Signaled:初始化为未触发状态
首先看下线程的调度:每个线程都有一个CONTEXT结构,保存在线程内核对象中。大约每隔20ms windows就会查看所有当前存在的线程内核对象。并在可调度的线程内核对象中选择一个,将其保存在CONTEXT结构的值载入cpu寄存器。这被称为上下文切换。大约又过20ms windows将当前cpu寄存器存回内核对象,线程被挂起。Windows再次检查内核对象,并在可调度的内核对象中选择一个进行调度。此过程不断重复直到系统关闭。Windows被称为抢占式多线程系统,系统可以在任何时刻停止一个线程而另行调度另外一个线程。我们对此可以有一些控制,但是权限很小。我们无法保证线程总在运行或者获得整个处理器。当然Windows提供很多API函数用于线程的挂起和恢复,睡眠,切换等。当然为了能够统计一个算法的执行时间,可以开启一个线程采用一些API函数精确统计去除线程切换而损失的时间。
其次分析一下CONTEXT结构以及如何获取和设置上下文CONTEXT结构:
CONTEXT结构包括以下部分:
CONTEXT_CONTROL:包含CPU的控制寄存器,比如指今指针,堆栈指针,标志和函数返回地址..AX, BX, CX, DX, SI, D
CONTEXT_INTEGER:用于标识CPU的整数寄存器.DS, ES, FS, GS
CONTEXT_FLOATING_POINT:用于标识CPU的浮点寄存器.
CONTEXT_SEGMENTS:用于标识CPU的段寄存器.SS:SP, CS:IP, FLAGS, BP
CONTEXT_DEBUG_REGISTER:用于标识CPU的调试寄存器.
CONTEXT_EXTENDED_REGISTERS:用于标识CPU的扩展寄存器I
CONTEXT_FULL:相当于CONTEXT_CONTROL or CONTEXT_INTEGER or CONTEXT_SEGMENTS,即这三个标志的组合
我们可以使用GetThreadContext函数来查看线程内核对象的内部,并获取当前CPU寄存器状态的集合。
BOOL GetThreadContext (
HANDLE hThread,
PCONTEXT pContext);
若要调用该函数,只需指定一个CONTEXT结构,对某些标志(该结构的ContextFlags成员)进行初始化,指明想要收回哪些寄存器,并将该结构的地址传递给GetThreadContext 。然后该函数将数据填入你要求的成员。
在调用GetThreadContext函数之前,应该调用SuspendThread,否则,线程可能刚好被调度,这样一来,线程的上下文就和所获取的信息不一致了。
示例代码如下:
CONTEXT Context; //定义一个CONTEXT结构
Context.ContextFlags = CONTEXT_CONTROL; //告诉系统我们想获取线程控制寄存器的内容
GetThreadContext(hThread, &Context); //调用GetThreadContext获取相关信息
Ps:在调用GetThreadContext函数之前,必须首先初始化CONTEXT结构的ContextFlags成员。
要获得线程的所有重要的寄存器(也就是微软认为最常用的寄存器),应该像下面一样初始化ContextFlags:
Context.ContextFlags = CONTEXT_FULL;
在WinNT. h头文件中,定义了CONTEXT_FULL为CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS。
当然,我们还可以通过调用SetThreadContext函数来改变结构中的成员,并把新的寄存器值放回线程的内核对象中
BOOL SetThreadContext (
HANDLE hThread,
CONST CONTEXT *pContext);
同样,如果要改变哪个线程的上下文,应该先暂停该线程。
CONTEXT Context; //定义一个CONTEXT结构
SuspendThread(hThread); //挂起线程
Context.ContextFlags = CONTEXT_CONTROL; //获取当前上下文的值
GetThreadContext(hThread, &Context);
Context.Eip = 0x00010000; //Eip字段存储的是指令指针,现在让指令指针指向地址 0x00010000;
Context.ContextFlags = CONTEXT_CONTROL;
SetThreadContext(hThread, &Context); //重新设置线程上下文
ResumeThread(hThread); //恢复线程,现在线程开始从0x00010000这个地方开始执行指令
当然操作系统对线程的还有其他的控制,为了提升CPU性能:1. 线程的优先级划分以及优先级的提升方法
2. CPU关联性,可以让线程在指定的CPU上运行,以提升程序和CPU性能。
线程能够共享进程的所有数据,比如全局变量,静态变量,全局数据结构,内核对象等,但是多线程同时访问会造成线程不安全,为此线程需要同步,下面介绍一下线程的同步方式:
用户模式下的同步方式:
1.原子访问:IterLocked系列函数。原理Interlocked函数会在总线上维持一个总线信号。
其中包括InterlockedExchangeAdd或者InterlockedExchangeAdd64递增第二参数值,InterlockedIncrement函数实现加1功能。
其余还有InterlockedExchange或者InterlockedExchange64或者InterlockedExchangePointer等。原子访问适合对整数或者bool型数据的操作。
2. 高速缓存行
芯片设计方式,可以避免过频繁的访问内存总线。为保证高速缓存行的性能,设计数据结构时应该保证只读数据和可读写数据分开。
3. Volatile变量,为了防止编译器优化,保证程序每次都从变量的内存处去读写数据,如果传入的是变量的地址就没有必要使用该变量了。
4. 关键段:将共享资源以原子方式进行访问,共享资源可以是多行代码。将关键段的全局变量CRITICAL_SECTION g_cs;
进入资源EnterCriticalSection,离开资源LeaveCriticalSection,缺点是无法再不同进程之间进行同步,容易造成死锁,因为不能设置关键段指定一个最长的等待时间
5. Slim读写锁,与关键段不同的是允许我们区分哪些想要读取资源的值的线程和想要更新资源的值的线程。让所有读取者线程在同一时刻访问共享资源,只有当写入者线程想要对资源进行更新时才进行同步。
6. 条件变量:有些情况下,如果如读写锁中的读取这线程没有数据可以读取,那么它应该将锁释放并等待,直到写入者线程产生了新的数据。写入者线程同样。其中Windows通过SleepConditionVariableCS或者SleepConditonVariableSRW函数。如果条件满足的时候会调用WakeConditionVariable或者WakeAllConditionVariable
内核模式下的同步方式:
1.等待函数 WaitForSingleObject(HANDLE hObject, DWORD dwilliseconds);等待函数意义是一个线程资源进入等待状态,直到指定的内核对象被触发为止。WaitForSingleObjects();
2. 事件内核对象:包含使用计数,标志自动重置事件和手动重置事件的bool值,事件有没有触发的bool值。创建一个事件CreateEvent或者CreateEventEx 打开一个事件OpenEvent. 设置事件的状态SetEvent 设置事件为未触发状态ResetEvent。注意:手动重置事件和自动重置的时间不太相同。
3. 可等待的计时器内核对象,它会在某个指定的时间触发或者每隔一段时间触发一次。创建可等待计时器,CreateWaitableTimer函数。或者打开已存在的可等待计时器OpenWaitableTimer
4. 信号量对象:用来对资源进行计数。
5.互斥量对象:确保一个线程独占对一个资源的访问。