目录
线程概述
线程内幕
线程相关函数详解
线程创建
线程睡眠(CPU时间片分配方式)
线程的优先级
线程的终止
(本章节中例子都是用 VS2005 编译调试的)
线程概述
组成
- 内核对象 操作系统用它来对线程实施管理,内核对象也是系统用来存放线程统计信息的地方,但创建线程时,系统创建一个内核对象,该线程内核对象不是线程本身,而是操作系统用来管理线程的较小数据结构,可以将线程内核对象视为有关于线程的统计信息组成的一个小型数据结构
- 线程栈 它用于维护线程在执行代码时需要的所有函数参数和局部变量
注意
- 线程总是在某个进程环境中创建的,而且会在这个进程内部销毁.系统从进程的地址工具中分配内存,供线程的栈使用,新的线程运行的进程环境与创建线程的环境相同,因此,新线程可以访问进程的内核对象的所有句柄(因为句柄表是针对每一个进程的,而不是针对每个线程的),进程中的所有内存和在这个相同进程中的所有线程的堆栈.这使得单个进程中的多个线程确实能非常容易的相互通信
- 线程只有一个内核和一个堆栈,保留记录很少,因此所需要的内存也很少,由于线程需要的开销比较少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程.因为一个进程创建一个虚拟地址空间需要大量的系统资源.系统中会发生大量的记录活动,而这些需要用到大量的内存.
获取当前线程句柄
获取当前线程句柄 GetCurrentThread (这个函数都返回的是一个伪句柄.它不会在主调进程的句柄表中新建句柄.而且调用这个函数,不会影响线程内核对象的使用计数器.如果调用 CloseHandle 函数关闭一个伪句柄, CloseHandle 只是简单地忽略此调用,并返回 FALSE,将伪句柄转换为真正的句柄: DuplicateHandle)
优点
- 每一个线程可以独立地完成一个任务.当该程序一直到多 CPU 的平台上时,其中的多个线程就可以真正并发进行地同时运行了
- 相对于进程来说
- 对进程创建来说,系统要分配进程很大的私有空间,当然它占用的资源也就很多,而对多线程程序来说,多个线程共享一个地址空间,所以占用资源较少
- 进程间切换时,需要交换整个地址空间,而线程之间切换时候只是切换执行环境,因此效率更高
单线程与多线程的执行区别
注意
多线程访问共享变量时要避免多个线程同时对共享变量进行操作
线程内幕
线程内核对象示意图
一旦创建了线程内核对象,系统就分配内存,供线程的堆栈使用,此内存是从进程的地址空间内分配的,因为线程没有自己的地址空间,因此线程没有自己的地址空间.线程堆栈始终是从高位内存地址向低位地址构建的
指令指针(IP)及 RtlUserThreadStart 函数
因为新线程的指令指针被设置为 RtlUserThreadStart,所以这个函数实际就是线程开始执行的地方,观察 RltUserThreadStart 的原型,你会以为他接收了两个参数,但这就暗示着该函数是从另一个函数调用的,而实情并非如此.新线程只是在此处产生并且开始执行.之所以能访问这个两个参数,是由于操作系统将值显示地写入线程堆栈(参数通常就是这样传给函数的)
新线程执行 RtlUserThreadStart 函数时候,将发生以下事情
-
- 围绕线程函数,会设置一个结构化异常处理(Structured Exception Handling,SEH)帧.这样一来线程执行期间所有产生的任何异常都能得到系统的默认处理.
- 系统调用线程函数,吧传给 CreateThread 函数的 pvParam 参数传递给它.
- 线程函数返回时,RtlUserThreadStart 调用 ExitThread,将你的线程函数的返回值传给它.线程内核对象的使用计数器递减,然后线程停止执行
- 如果线程产生了一个为被处理的异常,RtlUserThreadStart 函数所设置的 SEH 帧会处理这个异常.通常,这意味着系统会向用户显示一个消息框,而且当用户关闭此消息框时,RtlUserThreadStart 会调用 ExitProcess 来终止整个进程.而不只是终止有问题的线程
注意,在 RtlUserThreadStart 内,线程会调用 ExitThread 或者 ExitProcess.这意味着线程永远不能退出此函数;它始终在内部"消亡"
线程上下文
每个线程都有其自己的一组寄存器,称为线程上下文(context),上下文反应了当线程上一次执行时,线程的 CPU 寄存器的状态.系统使用 CONTEXT 结构记住线程状态.它也是唯一一个特定于 CPU 的.CONTEXT 组成:
-
- CONTEXT_INTERGER 标识 CPU 的整数寄存器
- CONTEXT_CONTROL 标识 CPU 的控制寄存器
- CONTEXT_FLOATING_POINT 标识 CPU 的浮点寄存器
- CONTEXT_SEGMENTS 标识 CPU 的段寄存器
- CONTEXT_DEBUG_REGISTERS 标识 CPU 的调试寄存器
- CONTEXT_EXTENDED_REGISTERS 标志 CPU 的扩展寄存器
挂起计数器(Suspend count)
作用
调用 CreateProcess 或者 CreateThread 时,系统将创建线程内核对象,并把挂起计数器初始化为1(这样确保线程或进程不会在初始化完全之前去执行任何代码).系统完成初始化之后,系统将检查 CREATE_SUSPENDED 标志是否已被传给 CreateThread 函数,如果此标记没有传递,系统将线程的挂起计数器递减至0,随后,线程就可以调度一个处理器去执行.然后,系统在实际的 CPU 寄存器中加载上一次在线程上下文中保存值,接着,线程可以在其进程的地址空间中执行代码并处理数据了.
ResumeThread/SuspendThread 函数
ResumeThread 函数用于减少线程挂起计数器,传入调用 CreateThread 时所返回的线程句柄予以实现,如果 ResumeThread 函数成功,它将返回线程的前一个挂起计数器.还可以通过 SuspendThread 来挂起线程,任何线程可以调用这个函数挂起另一个线程(只要有线程的句柄).与 ResumeThread 函数成功时候将返回线程的之前的挂起计数器
线程运行机制
操作系统为每个运行线程安排一定的CPU时间----时间片,系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因为时间相当短,多个线程频繁地发生切换,因此给用户的感觉就是好像多个线程同时运行一样,但是如果计算机有多个CPU,线程就能真正意义上的同时运行了.
大约每隔 20ms(GetSystemTimeAdjustment 函数的第二个参数的返回值),Windows 都会查看所有当前存在的线程内核对象.在这个对象中,只有一些被认为是可调度的.Windows 在可调度的线程内核中选择一个,并将上次保存在线程上下文的值载入 CPU 寄存器.然后线程执行代码,并且在进程的地址空间操作数据.有过了大约 20ms,Windows将 CPU 寄存器存回线程的上下文,线程不再运行.系统再次检查剩下的可调度线程内核对象,选择另一个线程的内核对象,将线程的上下文载入 CPU 寄存器,然后继续,载入线程上下文,让线程运行,保存上下文并重复的操作在系统启动的时候就开始,然后这样的操作不断重复,直至系统关闭
主线程与非主线的区别
- 在默认情况下,主线程入口函数必须命名为 main,wmain,WinMain,wWinMain,(除非我们用/ENTRY:链接器选项来指定另一个函数作为入口函数),而线程函数可以任意命名;
- 因为主线程入口函数有字符串参数,所以它提供了 ANSI/Unicode 版本提供我们选择 main/wmain 和 WinMain/wWinMain.相反,线程函数只有一个参数,而且其意义由我们(而非操作系统)来定义,因此我们不必担心 ANSI/Unicode 问题
- 线程函数必须返回一个值,它会成为该线程的退出代码,这类似于 C/C++ 运行库的策略:令主线程的退出代码成为进程的退出代码
- 线程函数(实际包括所有函数)应该尽可能使用函数参数和局部变量.使用静态变量和全局变量时,多个线程可以同时访问这些变量,这样可能会破坏变量中保存的内容.然而,由于函数的参数和局部变量时在线程上创建的,因此,不可能被其他线程破坏
进程创建相关函数
[creathread][线程入口函数原型][Sleep][CloseHandle]
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
参数说明
- lpThreadAttributes 指向SECURITY_ATTRIBUTES结构体指针,这里可以传递NULL,让该线程使用默认的安全性.但是,如果希望所有的子进程能够继承该线程对象的句柄,就必须设定一个SECURITY_ATTRIBUTES结构体,将它的bInheritHandle成员初始化为TRUE
- dwStackSize: 设置线程初始堆栈大小,即线程可以将多少空间用于自己的栈,以字节为单位,系统会将这个参数值四舍五入到最接近页面大小,当保留地址空间的一块区域时,系统要确保该区域的大小是系统页面的大小的整数倍(页面是系统管理内存时使用的的内存单位,不同CPU其页面大小不同)
- lpStartAddress: 指向应用程序定义的LPTHREAD_START_ROUTINE类型的函数指针,这个函数将由新线程执行,表明新线程的起始地址
- lpParameter: 对于main函数来说可以寄售命令行的参数.同样我们也可以通过这个参数给创建的新线程传递参数.参数的值可以是一个数值,也可以使一个指向其他信息的指针
- dwCreationFlags: 设置用于控制线程创建的附加标记,它可以是两个值得其中一个
- CREATE_SUSPENDED 线程创建后处于暂停状态,直到程序调用了ResumeThread函数位置)
- 0 线程在创建后立即运行
- lpThreadId: 这个参数是一个返回值,它指向一个变量,来接收线程ID,当创建一个线程时,系统会为该线程分配一个ID(在windows 2000和windows NT4下,可以为NULL,但在windows95 和 windows 98下此参数不能为NULL)
返回值
新建立的线程句柄
函数原型
DWORD WINAPI threadProName(LPVOID lpParameter);
参数说明
- lpParamer: 为创建线程CreateThread函数中的lpParameter参数的值
函数原型
void Sleep(DWORD dwMilliseconds);
参数说明
- dwMilliseconds 指定线程的睡眠时间,单位为毫秒
函数原型
BOOL CloseHandle( HANDLE hObject // handle to object to close );
参数说明
- hObject 要关闭的句柄
返回值
操作成功时候返回非零值,操作失败返回0
用途说明
若想要子线程可以运行而主线程不执行操作①可以让主线程睡眠,②可以让主线程执行循环空操作,但是用这种方式的话,对于主线程来说主线程是可以运行的,并且它会占有一定的CPU时间,这样会影响到MultiThread程序的执行效率.
说明 CreateThread启动了一个线程,同时产生一个句柄让你好操纵这个线程,如果你不要用这个句柄了就CloseHandle关掉它.调用这个CloseHandle并不意味着结束线程,而是表示不关心此句柄的状态了,也就无法控制子进程的线程了.如果需要关心,可以在子进程结束后再CloseHandle,但一定得CloseHandle.
CloseHandle使指定的句柄无效,减少对象的句柄计数,进行对象保持检验.当对象的最后一个句柄关闭时,对象将从系统中删除.关闭一个线程句柄并 不会终止一个线程,要释放一个线程对象,必须terminate线程,然后关闭所有的线程句柄.用CloseHandle只能关闭由CreateFile 函数返回的句柄.用FindClose来关闭由FindFirstFile返回的句柄.
线程创建
C/C++ 运行库创建线程函数 _beginthreadex
_beginthreadex 注意
-
- 每个线程都有自己专用的 _tiddata 内存块,它们是从 C/C++ 运行库的堆上分配的.
- 传给 _beginthreadex 的线程函数的地址保存在 _tidata 内存块中.(_tiddata 结构在 Mtdll.h 文件的 C++ 源代码中)
- _beginthreadex 确实会在内部调用 CreateThread ,因为操作系统只知道用这种方式来创建一个新线程
- CreateThread 函数被调用时,传递给它的函数地址是 _threadstartex(而非 pfnStartAddr).另外,参数地址是 _tiddata 结构的地址,而非 pvParam
- 如果一切顺利,会返回线程句柄,就像 CreateThread 那样.任何操作失败,会返回0
- 新线程首先执行 RtlUserThreadStart(在 NTDLL.dll),然后在跳转到 _threadstartex
- _threadstartex 唯一的参数就是新线程的 _tiddata 内存块地址
- TlsSetValue 是一个操作系统的函数,它将一个值域主调线程关联起来.这就是所谓的线程句柄存储(Thread Local Storage,TLS),_threadstartex 函数将 _tiddata 内存块与新建立线程关联起来.
- 在无参数的辅助函数 _callthreadstartex 中,有一个 SEH 帧,它将预期要执行的线程函数包围起来.这个帧处理着与运行库有关的许多事情---比如运行时错误(如抛出未被捕捉的C++异常)---和 C/C++ 运行库的 signal 函数.这一点相当重要.如果用 CreateThread 函数新建了一个线程,然后调用 C/C++ 运行库的 signal 函数,那么 signal 函数不能正常工作
- 预期要执行的线程会被调用,并向其传递预期的参数.前面讲过,函数的地址和参数由 _beginthreadex 保存在 TLS 的 _tiddata 数据块中,并在 _callthreadstartex 中从 TLS 中获得.
- 线程函数的返回值被认为是线程的退出代码
- 注意 _callthreadstarex 不是简单地返回到 _threadstartex 不是简单地返回到 _threadstartex,继而到 RtlUserThreadStart,如果是那样的话,线程会终止运行,其退出代码也会被设置,但线程的 _tiddata 内存块不会被销毁,这样会导致内存泄露.为了防止这个问题, _threadstartex 调用了 _endthreadex (也是一个 C/C++ 运行库函数),并向其传递退出代码.
_endthreadex 注意
-
- C 运行库的 _getptd_noexit 函数在内部调用操作系统的 TlsGetValue 函数,后者获取主调线程的 _tiddata 内存块地址.
- 然后,_endthreadex 将此数据块释放,并调用操作系统的 ExitThread 函数来时间销毁线程.担任,它会传递并正确设置退出代码
_endthread 注意
函数在调用 ExitThread 函数前会先调用 CloseHandle
创建过程:
程序样例
用 CreateThread 创建线程
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI FunProc(LPVOID lpParameter); void main() { HANDLE hThread; hThread=CreateThread(NULL,0,FunProc,NULL,0,NULL); CloseHandle(hThread); Sleep(500); system("pause"); } DWORD WINAPI FunProc(LPVOID lpParameter) { cout<<"创建子线程成功!"<<endl; return 0; }
运行结果:
用 _beginthread 创建线程
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> #include <process.h> using namespace std; void FunProc(void* lpParameter); void main() { HANDLE hThread; hThread =(HANDLE)_beginthread(FunProc,0,NULL); /* CloseHandle(hThread); //如果你调用了 CloseHandle,那么线程会在退出的时候出错,因为线程在执行函数返回的时候会回到 _beginthread 函数中 //然后,_beginthread 函数会调用 _endthread 函数终止线程 //而 _endthread 函数会调用 ExitThread 函数且在此之前前会先调用 CloseHandle */ Sleep(500); system("pause"); } void FunProc(void* lpParameter) { cout<<"创建子线程成功!"<<endl; }
运行结果:
用 _beginthreadex 创建线程
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> #include <process.h> using namespace std; unsigned __stdcall FunProc(void* lpParameter); void main() { HANDLE hThread; hThread =(HANDLE)_beginthreadex(NULL,0,FunProc,NULL,0,NULL); CloseHandle(hThread); Sleep(500); system("pause"); } unsigned __stdcall FunProc(void* lpParameter) { cout<<"创建子线程成功!"<<endl; return 0; }
运行结果:
线程睡眠(CPU时间片分配方式)
Unix系统
Unix系统使用的是时间片算法
定义
在时间片算法中,所有的进程排成一个队列.操作系统按照他们的顺序,给每个进程分配一段时间,即该进程 允许运行的时间.如果在 时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程.如果进程在时间片结束前阻塞或结束,则CPU当即进行切换.调度程序所要做的就是维护一张就绪进程列表,,当进程用完它的时间片后,它被移到队列的末尾
说明
我们用分蛋糕的场景来描述这这种算法.假设有源源不断的蛋糕(源源不断的时间),一副刀叉(一个CPU),10个等待吃蛋糕的人(10 个进程)
Windows 操作系统来负责分蛋糕的,那么场面就很有意思了.他会这样定规矩:我会根据你们的优先级、饥饿程度去给你们每个人计算一个优先级.优先级最高的那个人,可 以上来吃蛋糕——吃到你不想吃为止.等这个人吃完了,我再重新根据优先级、饥饿程度来计算每个人的优先级,然后再分给优先级最高的那个人.这样看来,这个场面就有意思了——可能有些人是PPMM,因此具有高优先级,于是她就可以经常来吃蛋糕.可能另外一个人是个丑男,而去很ws,所以优先级 特别低,于是好半天了才轮到他一次(因为随着时间的推移,他会越来越饥饿,因此算出来的总优先级就会越来越高,因此总有一天会轮到他的).而且,如果一不 小心让一个大胖子得到了刀叉,因为他饭量大,可能他会霸占着蛋糕连续吃很久很久,导致旁边的人在那里咽口水...而且,还可能会有这种情况出现:操作系统现在计算出来的结果,5号PPMM总优先级最高,而且高出别人一大截.因此就叫5号来吃蛋糕.5号吃了一小会儿, 觉得没那么饿了,于是说“我不吃了”(挂起).因此操作系统就会重新计算所有人的优先级.因为5号刚刚吃过,因此她的饥饿程度变小了,于是总优先级变小 了;而其他人因为多等了一会儿,饥饿程度都变大了,所以总优先级也变大了.不过这时候仍然有可能5号的优先级比别的都高,只不过现在只比其他的高一点点 ——但她仍然是总优先级最高的啊.因此操作系统就会说:5号mm上来吃蛋糕……(5号mm心里郁闷,这不刚吃过嘛……人家要减肥……谁叫你长那么漂亮,获 得了那么高的优先级). 那么,Thread.Sleep 函数是干吗的呢?还用刚才的分蛋糕的场景来描述.上面的场景里面,5号MM在吃了一次蛋糕之后,觉得已经有8分饱了,她觉得在未来的半个小时之内都不想再 来吃蛋糕了,那么她就会跟操作系统说:在未来的半个小时之内不要再叫我上来吃蛋糕了.这样,操作系统在随后的半个小时里面重新计算所有人总优先级的时候, 就会忽略5号mm.Sleep函数就是干这事的,他告诉操作系统“在未来的多少毫秒内我不参与CPU竞争”.
Windows系统
Windows系统使用的是抢占式
定义
所谓抢占式操作系统,就是说如果一个进程得到了 CPU 时间,除非它自己放弃使用 CPU ,否则将完全霸占 CPU .因此可以看出,在抢 占式操作系统中,操作系统假设所有的进程都是“人品很好”的,会主动退出 CPU . 在抢占式操作系统中,假设有若干进程,操作系统会根据他们的优先级、饥饿时间(已经多长时间没有使用过 CPU 了),给他们算出一 个总的优先级来.操作系统就会把 CPU 交给总优先级最高的这个进程.当进程执行完毕或者自己主动挂起后,操作系统就会重新计算一 次所有进程的总优先级,然后再挑一个优先级最高的把 CPU 控制权交给他
说明
我们用分蛋糕的场景来描述这这种算法.假设有源源不断的蛋糕(源源不断的时间),一副刀叉(一个CPU),10个等待吃蛋糕的人(10 个进程)
Unix 操作系统来负责分蛋糕,那么他会这样定规矩:每个人上来吃 1 分钟,时间到了换下一个.最后一个人吃完了就再从头开始.于是,不管这10个人是不是优先级不同、饥饿程度不同、饭量不同,每个人上来的时候都可以吃 1 分钟.当然,如果有人本来不太饿,或者饭量小,吃了30秒钟之后就吃饱了,那么他可以跟操作系统说:我已经吃饱了(挂起).于是操作系统就会让下一个人接着来
两个问题
假设现在是 2008-4-7 12:00:00.000,如果我调用一下 Thread.Sleep(1000) ,在 2008-4-7 12:00:01.000 的时候,这个线程会不会被唤醒?
不一定.因为你只是告诉操作系统:在未来的1000毫秒内我不想再参与到 CPU竞争.那么1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束;况 且,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去. 与此相似的,Thread有个Resume函数,是用来唤醒挂起的线程的.好像上面所说的一样,这个函数只是“告诉操作系统我从现在起开始参与CPU竞争了”,这个函数的调用并不能马上使得这个线程获得CPU控制权
某人的代码中用了一句看似莫明其妙的话:Thread.Sleep(0) .既然是 Sleep 0 毫秒,那么他跟去掉这句代码相比,有啥区别么?
有,而且区别很明显.假设我们刚才的分蛋糕场景里面,有另外一个PPMM 7号,她的优先级也非常非常高(因为非常非常漂亮),所以操作系统总是会叫道她来吃蛋糕.而且,7号也非常喜欢吃蛋糕,而且饭量也很大.不过,7号人品很好,她很善良,她没吃几口就会想:如果现在有别人比我更需要吃蛋糕,那么我就让给他.因此,她可以每吃几口就跟操作系统说:我们来重新计算一下所有人的总 优先级吧.不过,操作系统不接受这个建议——因为操作系统不提供这个接口.于是7号mm就换了个说法:“在未来的0毫秒之内不要再叫我上来吃蛋糕了”.这个指令操作系统是接受的,于是此时操作系统就会重新计算大家的总优先级——注意这个时候是连7号一起计算的,因为“0毫秒已经过去了”嘛.因此如果没有比 7号更需要吃蛋糕的人出现,那么下一次7号还是会被叫上来吃蛋糕. 因此,Thread.Sleep(0)的作用,就是“触发操作系统立刻重新进行一次CPU竞争”.竞争 的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权.这也是我们在大循环里面经常会写一句Thread.Sleep(0) ,因为这样就给了其他线程比如Paint线程获得CPU控制权的权力,这样界面就不会假死在那里. 末了说明一下,虽然上面提到说“除非它自己放弃使用 CPU ,否则将完全霸占 CPU”,但这个行为仍然是受到制约的——操作系统会监控你霸占CPU的情况,如果发现某个线程长时间霸占CPU,会强制使这个线程挂起,因此在实际上不 会出现“一个线程一直霸占着 CPU 不放”的情况.至于我们的大循环造成程序假死,并不是因为这个线程一直在霸占着CPU.实际上在这段时间操作系统已经进行过多次CPU竞争了,只不过其他 线程在获得CPU控制权之后很短时间内马上就退出了,于是就又轮到了这个线程继续执行循环,于是就又用了很久才被操作系统强制挂起...因此反应到界面 上,看起来就好像这个线程一直在霸占着CPU一样. 末了再说明一下,文中线程、进程有点混乱,其实在Windows原理层面,CPU竞争都是线程级的,本文中把这里的进程、线程看成同一个东西就好了
Windows Sleep 注意
- 调用 Sleep 函数,将使线程自愿放弃属于它的时间片中剩下的步伐
- 系统设置线程不可调度的时间只是"近似于"所设定的毫秒数.没错,如果告诉系统睡眠 100ms,那么线程将睡眠差不多这么长的时间,但是可能会长达数秒甚至数分钟.别忘了,Windows 不是实时操作系统.我们的线程可能准时醒来,但是实际情况取决于系统中其他线程的运行情况
- 可以给 Sleep 传入 0 .这是告诉系统,主调线程放弃了时间片剩余部分,它强制系统调用其他线程.但是系统有可能重新调用刚刚调用 Sleep 的那个线程.如果没有相同或者较高优先级的可调度线程时,就会发生这样的事情
SwitchToThread
系统提供了 SwitchToThread 的函数,如果存在另一个可调度线程,那么系统会让此线程运行,调用这个函数时,系统查看是否存在正急需 CPU 时间的饥渴线程.如果没有,SwitchToThread 立即返回.如果存在,SwitchToThread 将调度该线程(其优先级可能比 SwitchToThread 的主调线程低)
通过这个函数,需要某个资源线程可以强制一个可能拥有该资源的低优先级的线程放弃资源.如果在调用 SwitchToThread 时没有其他线程可以运行,则函数将返回 FALSE,否则,函数将返回一个非零值.
SwitchToThread 允许执行低优先级线程,Sleep 会立即重新调用主调线程,即使低优先级线程还处于饥饿状态
线程的优先级
概述
每个线程都被赋予0(最低)~31(最高)的优先级数,当较高优先级的线程占用了 CPU 时间,致使较低优先级的线程无法运行时,我们就称这种情况为饥饿.
较高优先级的线程总会是抢占较低优先级的线程,不管较低优先级的线程是否在执行(例如,有一个优先级为 5 的线程在运行,而系统确定有较高优先级线程的线程已准备可以运行,它会立即暂停较低优先级的线程[即使后者的时间片还没有用完],并将 CPU 分配给较高优先级的线程,该线程获得一个完整的时间片)
当系统检测到有线程已经处于饥饿状态3到4秒时,它会动态将饥饿线程的优先级提升为15,并允许该线程运行两个时间片.当两个时间片结束时,线程的优先级立即恢复到基本的优先级
注意在窗体程序中,进程的主线程调用了 GetMessage,而系统看到并没有消息等待处理,它就会暂停这个线程,取消这个线程当前时间片的剩余时间,并立即将 CPU 分配给另一个等待中的线程,一旦消息进入线程的消息队列,系统会知道主线程不应该暂停了,如果没有较高优先级的线程需要执行,系统将给它分配 CPU
如果用户需要使用某个进程的窗口,这个进程就称为前台进程(foreground process),而所有其他的进程称后天进程(background process),系统给前台的线程分配比一般情况下更多的时间片,这种微调只在前台进程是 normal 优先级时才进行.如果其他其他优先级,则不会进行微调
进程优先级类(priority class)
- idle 此进程中的线程在系统空闲时运行.屏幕保护程序,后台实用程序和统计数据收集软件通常使用该进程
- below normal 此进程中的线程运行在 normal 和 idle 优先级之间
- normal 此进程中的线程无需特殊调度
- above normal 此进程中的线程运行在 normal 和 high 优先级类之间
- high 此进程中的线程必须立即响应事件,执行实时任务.任务管理器运行在这一级,因此用户可以通过它结束失控的程序(在必要时候才使用)
- real-time 此进程中的线程必须立即响应事件,执行实时任务,此进程中的线程还是会抢占操作系统的组件的 CPU 时间,使用该优先级类需要极为小心(应该避免使用)
进程不能运行在 real-time 优先级类,除非用户有 Increase Scheduling Priority(提高计划优先级)特权,默认情况下,隶属于管理员或者高级用户组都具有这一权限
线程的优先级
- idle 对于 real-time 优先级类,线程运行在 16,所有其他优先级运行在 1
- lowest 线程运行在低于 normal 之下的两个级别
- below normal 线程运行在低于 normal 之下的一个级别
- normal 线程运行在进程 normal 级别上
- above normal 线程运行在高于 normal 之上一个级别
- highest 线程运行在高于 normal 之上两个级别
- time-critical 对于 real-time 优先级类,线程运作在31上,所有其他优先级运行在15
应用程序的开发人员无需处理优先级,而是由系统将进程的优先级类和线程的相对优先级映射到一个优先级值
CreateThread 总是创建相对线程优先级为 normal 的新线程.要使线程以 idle 优先级执行,我们需要在调用 CreateThread 时传入 CREATE_SUSPENDED 标志,这将阻止线程执行任何代码.然后我们调用 SetThreadPriority 将线程改为 idle 相对线程优先级.接着调用 ResumeThread,线程就改为可调度的了.
线程优先值映射关系
相对进程的线程优先级 | ||||||
进程优先级类 | idle | below normal | normal | above normal | high | real-time |
time-critical |
15 | 15 | 15 | 15 | 15 | 31 |
highest | 6 | 8 | 10 | 12 | 15 | 26 |
above normal | 5 | 7 | 9 | 11 | 14 | 25 |
normal | 4 | 6 | 8 | 10 | 13 | 24 |
below normal | 3 | 5 | 7 | 9 | 12 | 23 |
lowest | 2 | 4 | 6 | 8 | 11 | 22 |
idle | 1 | 1 | 1 | 1 | 1 | 16 |
线程优先级是相对于进程优先级的.如果改变进程优先级,线程的相对有优先级不变,但是优先值将变化
应用程序也无法获得以下优先级:17,18,19,20,21,27,28,29或者30.当然,如果编写的是运行在内核模式的设备驱动程序,那么我们可以获得这些优先级,用户模式的应用程序是不能获得这些优先级的.还要注意 real-time 优先级类的线程,其优先级值不能低于16,同类,非 real-time 优先级线程的优先级不能高于15
系统动态调度线程优先值
系统通过线程的相对优先级加上线程所属进程额优先级来确定线程的优先值.有时候,这也被线程的基本优先级值(base priority level).偶尔,系统也会提升一个线程的优先级(例如,high 优先级进程中的一个线程优先级为 normal 的线程,其最基本优先值为13.如果用户敲击一个键,系统会在线程的队列中放入一个 WM_KEYDOWN 消息.因为有消息出现在线程队列中,线程就成为可调度的了.而且,键盘设备驱动程序将使系统临时提升线程优先级.隐藏线程优先级可能会提升2,从而使当前的优先级达到15,线程在优先级15时分得一个时间片.在该时间片结束后,系统将线程的优先级减1,所以下一个时间片线程的优先级为14,线程的第三个时间片优先级13执行.以后的时间片将保持在13,即线程的基本优先级)
线程的当前优先级不会低于线程的基本优先级.而且使线程可调度的设备驱动程序能够决定提升的幅度
系统只提升优先级值在 1~15 的线程.事实上,正因为如此,这个范围被称为动态优先级范围(dynamic priority range).而且,系统不会吧线程的优先级提升到实时范围(高于15).因为实时范围内的线程执行大多数操作系统功能,对提升设置上限,可以防止应用程序影响操作系统.而且,系统不能动态提升实时范围(16~31)的线程.
设置进程/线程优先级
- 进程
- 在进程运行前可以用在 CreateProcess 函数中设置 fdwCreate
- 一旦程序运行,便可以通过 SetPriorityClass 来改变自己的优先级,
- 用 GetPriorityClass 获得进程优先级的相应函数
- 线程
- 设置优先级 SetThreadPriority
- 获得优先级 GetThreadPriority
设置进程/线程优先级
- SetProcessPriorityBoost 允许或禁止系统提升一个进程中所有线程的优先级
- SetThreadPriorityBoost 允许或禁止提升某个线程的优先级
- GetProcessPriorityBoost 判断进程是否启用优先级提升
- GetThreadPriorityBoost 判断线程是否启动优先级提升
I/O 请求优先级
设置线程优先级将影响系统如何给线程分配 CPU 资源.但是,线程还要执行 I/O 请求,以对磁盘文件读写数据.如果一个低优先级线程获得 CPU 时间,它可以很轻易地在很短时间内将成百上千个 I/O 请求一般都需要时间进行处理,可能低优先级线程会挂起优先级的线程,使他们无法完成任务,从而显著影响系统响应性
线程可以在进行 I/O 请求时设置优先级了.我们可以通过调用 SetThreadPriority 并传入 THREAD_MODE_BACKGROUND_BEGIN 来告诉 Windows 线程应该发送低优先级的 I/O 请求.通过调用 SetThreadPriority ,并传入 THREAD_MODE_BACKGROUND_END,让线程进行 normal 优先级 I/O 请求.调用 SetThreadPriority 并传入上面两个标志之一时,还必须传入主调线程的句柄(通过调用 GetCurrentThread 返回得到),系统不允许线程改变另一个线程的 I/O 优先级.
线程的终止
终止运行线程
- 线程函数返回(推荐)
- 线程通过调用 ExitThread 函数,"杀死"自己(避免)
- 同一个进程或另一个进程中的线程调用 TerminateThread 函数(避免)
- 包含线程的进程终止
线程函数返回(而非其他终止线程的方式)的清理工作
- 线程函数中创建的 C++ 对象都通过其析构函数被正确销毁
- 操作系统正确释放线程栈使用的内存
- 操作系统把线程的退出代码(在线程的内核对象中维护)设为线程函数返回值
- 系统递减线程的内核对象的使用计数器
让线程函数返回,可以确保以下正确的应用程序清理工作都得以执行
TerminateThread/ExitThread 函数
- ExitThread 该函数将终止线程运行,并导致操作系统清理该线程使用的所有操作系统资源.但是你的 C/C++ 资源(如C++类对象)不会被销毁.
- 如果通过返回或调用 ExitThread 函数的方式来终止一个线程的运行,该线程的堆栈也会被销毁,但是,如果使用的是 ExitThread,那么触发拥有此线程的进程终止运行,否则系统不会销毁这个线程的堆栈
线程终止运行时
- 线程拥有的所有的用户对象句柄会被释放.在 Windows 中,大多数对象都是由包含了"创建这些对象的线程"的进程拥有的.但一个线程有两个用户对象:窗口(Window)和挂钩(hook).一个线程终止运行时,系统会自动销毁由线程创建或安装的任何窗口.并卸载有线程创建或安装的任何挂钩.其他对象只有在线程的进程终止时候才被销毁.
- 线程退出代码从 STILL_ACTIVE 变成传给 ExitThread 或 TerminateThread 的代码.
- 线程内核对象的状态变为触发状态.
- 如果线程是进程中的最后一个活动线程,系统任务进程也终止了.
- 线程内核对象的使用计数器递减1.
注意
- 线程终止时,其关联的线程对象不会自动释放,除非对这个对象的所有未结束的引用都关闭了.
- 一旦线程不再运行,系统中就没有别的线程可以再用该线程的句柄.但是,其他线程可以调用 GetExitCodeThread 来检查 hThread 所标识的那个线程是否已终止
代码样例
程序源码:
#include <windows.h> #include <iostream> #include <cstdlib> using namespace std; DWORD WINAPI FunProc(LPVOID lpParameter); class test{ public: ~test(){cout<<"this is test destructor"<<endl;} }; void main() { HANDLE hThread; hThread=CreateThread(NULL,0,FunProc,NULL,0,NULL); Sleep(50); TerminateThread(hThread,0); CloseHandle(hThread); system("pause"); } DWORD WINAPI FunProc(LPVOID lpParameter) { test a; cout<<"创建子线程成功!"<<endl; Sleep(50); //ExitThread(0); //下面输出这句话没有机会执行,而且 a 对象也无法调用的到析构函数 cout<<"子线程终止失败!"<<endl; return 0; }
运行代码: