线程调度概述
- 每个线程都有一个上下文,这个上下文保存在内核对象中。它反应了线程上一次执行时CPU寄存器的状态。
- 大概每个20ms(GetSystemTimeAdjustment函数的第二个参数的返回值),Windows都会查看所有当前存在的线程内核对象。
这些对象中,只有一些被认为是可调度的。
对于不可调度的线程,除了那些在创建的时候被挂起的,还有很多线程无法调度的,因为它们都在等待某种事情发生。 - Windows在可调度的线程内核对象中选择一个,并将上次保存在线程上下文中的值载入CPU寄存器。
这个操作叫做上下文切换。
Windows实际上会记录每个线程运行的次数的(用Spy++可以看到)。 - Windows之所以被称为抢占式多线程操作系统,是因为系统可以在任何时刻停止一个线程而另行调度另一个线程。
线程的挂起和恢复
- 线程内核对象中有一个值用来表示线程的挂起计数。
调用CreateProcess或者CreateThread的时候,系统将创建内核对象,并把挂起计数初始化为1。
这样,就不会给这个线程调度CPU了。因为,线程的初始化需要时间,在它初始化好之后再执行它。 - 线程初始化好之后,CreatePtocess或CreateThread函数将查看是否有CREATE_SUSPENDED标志传入。
- 如果有,函数会返回并让新的线程处于挂起状态。
- 如果没有,线程的挂起计数递减为0。当线程的挂起计数为0时,线程就成为可调度的了,除非它还在等待某个事件发生。
- 创建一个挂起状态的线程,我们可以在线程执行任何代码之前改变它的环境。
改变了线程的环境之后,必须使其变为可调度的。通过ResumeThread函数可以做到。
1 2 3 |
// 成功,返回线程之前一个挂起计数 // 失败,返回0xFFFFFFFF DWORD ResumeThread(HADNLE hThread); |
- 一个线程可以被挂起多次。
一个线程最多可以被挂起MAXIMUM_SUSPEND_COUNT(127)次。
如果一个线程被挂起了3次,则在它有资格让系统为它分配CPU之前必须恢复3次。 - 除了在创建线程的时候传入CREATE_SUSPENDED标志来挂起线程外,还可以使用SuspendThread函数来挂起线程。
任何线程都可以通过这个函数来挂起另一个线程(只要有线程的句柄)。
所以,线程可以通过这个函数把自己挂起,但无法把自己恢复。
1 2 |
// 成功,返回线程之前的挂起计数 DWORD SuspendThread(HANDLE hThread); |
- 需要注意的是,在内核模式下的执行情况而且,SuspendThread是异步的,但是在线程恢复之前,它是无法在用户模式下执行的。
进程的挂起和恢复
- 其实,Windows中不存在挂起和恢复进程的概念。因为系统从来不会给进程调度CPU时间。
- 如何挂起一个进程中的所有线程,在一个(特殊)情况下:
- 调试器处理WaitForDebugEvent返回的调试事件时,Windows将冻结被调试进程中的所有线程,直到调试器调用ContinueDebugEvent。
- 一个适用许多情况的挂起进程的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
VOID SuspendProcess(DWORD dwProcessID, BOOL fSuspend) { HANGLE hSnapshot = CreateToolhelp32SnapShot( TH32CS_SNAPTHREAD, dwProcessID); if (hSnapshot != INVALID_HANDLE_VALUE) { THREADENTRY32 te = { sizeof(te)}; BOOL fOk = Thread32First(hSnapshot, &te); for (; fOk; fOk = Thread32Next(hSnapshot, &te)) { if (te.th32OwnerProcessID == dwProcessID) { HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID); if (hThread != NULL) { if (fSuspend) { SuspendThread(hThread); } else { ResumeThread(hThread); } } CloseHandle(hThread); } } CloseHandle(hSnapshot); } } |
睡眠
- 线程可以告诉系统,在一段时间内,自己不需要调度了。
1 |
VOID Sleep(DWORD dwMilliseconds); |
- 这个函数将线程自己挂起dwMilliseconds那么长时间。
- 需要注意的是:
- 掉了Sleep函数,将使线程自愿放弃属于它的时间片中剩下的部分。
- 系统设置线程不可调度的时间只是“近似于”所设定的毫秒数。
如果告诉系统向睡眠100ms,那么线程可能将睡眠差不多这么长时间,但是可能会长达数秒甚至数分钟。(Windows不是实时操作系统,我们的线程可能准时醒来,但实际情况取决于系统中其他线程的运行情况) - 可以传参INFINITE来告诉系统永远不要调度这个进程,但是这么做没什么用处。因为让线程退出并将其栈和内核对象返还给系统,要更好。
- 可以传参0给Sleep,会告诉系统主调线程放弃时间片的剩余部分,它强制系统调度其他线程。
但是系统有可能重新调度刚刚Sleep(0)的那个线程。
切换到另一个线程
- 系统提供了一个SwitchToThread的函数,如果存在另一个可调度线程,那么系统就会让此线程运行。
1 |
BOOL SwitchToThread(); |
- 调用这个函数时,系统会查看是否存在正急需CPU时间的饥饿线程。
- 如果没有的话,这个函数会立即返回。
- 如果有的话,将会调度该线程。
- 饥饿线程可以运行一个时间量,然后系统调度程序恢复正常运行。
- 需要某个资源的线程可以通过这个函数来强制一个可能拥有该资源的低优先级的线程放弃资源。
- 如果调用SwitchToThread时,没有其他线程可以运行,函数将返回FALSE,否则返回一个非0值。
- 调用SwitchToThread与调用Sleep类似,传入0ms超时即可。
和Sleep不同的是,SwitchToThread允许执行低优先级线程,而Sleep会立即重新调度主调线程。
在超线程CPU上切换到另一个线程
- 超线程(hyper-threading)是Xeon,Pentium4和更新的CPU支持的一种技术。
- 超线程处理器芯片有多个“逻辑”CPU,每个都可以允许一个线程。
- 每个线程都有自己的一组体系结构状态(一组寄存器),但是所有线程共享主要的执行资源,比如CPU高速缓存。
- 当一个线程中止时,CPU自动执行另一个线程,无需操作系统干预。
- 只有在缓存为命中、分支预测错误和需要等待前一个指令的结果等情况下,CPU才会暂停。
- 在超线程CPU上执行旋转循环时,需要我们强制当前线程暂停,使另一个线程可以访问芯片的资源。
线程的执行时间
- 计算一个线程执行某项任务需要消耗多长时间(前提是执行的代码不会被中断):
1 2 3 4 |
// 获取当前时间 ULONGLONG qwStartTime = GetTickCount64(); ULONGLONG qwElapsedTime = GetTickCount64() - qwStartTime; |
- 获取线程以获得的CPU时间:
- 参数一:创建时间:线程创建时间的绝对值
- 参数二:退出时间:线程退出时间的绝对值
- 参数三:内核时间:表示线程执行内核模式下的操作系统代码所用时间的绝对值:100ns为单位
- 参数四:用户时间:表示线程执行应用代码所用时间的绝对值:100ns为单位
1 2 3 4 5 |
BOOL GetThreadTimes(HANDLE hThread, PFILETIME pftCreationTime, PFILETIME pftExitTime, PFILETIME pftKernelTime, PFILETIME pftUserTime); |
- 确定一个算法所需要的时间:
1 2 3 |
__int64 FileTimeToQuadWord(PFILETIME pft) { return (Int64ShllMod32(pft->dwHighDateTime, 32)) | pft->dwLowDateTime); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void PerformLongOperation() { FILETIME ftKernelTimeStart, ftKernelTimeEnd; FILETIME ftUserTimeStart, ftUserTimeEnd; FILETIME ftDummy; __int64 qwKernelTimeElapsed, wqUserTimeElapsed, wqTotalTimeElapsed; // 获取开始时间 GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeStart, &ftUserTimeStart); // 获取终止时间 GetTheradTimes(GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeEnd, &ftUserTimeEnd); qwKernelElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) - FileTimeToQuadWord(&ftKernelTimeStart); qwUserElapsed = FileTimeToQuadWord(&ftUserTimeEnd) - FileTimeToQuadWord(&ftUserTimeEnd); qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed; } |
- 关于GetProcessTimes:返回的时间用于一个指定进程中的所有线程(即使线程已经终止)。
1 2 3 4 5 |
BOOL GetProcessTimes(HANDLE hProcess, PFILETIME pftCreationTime, PFILETIME pftExitTime, PFILETIME pftKernelTime, PFILETIME pftUserTime); |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Cef:沙盒、CefApp、CefClient02/29
- ♥ Dump分析:未捕获的异常,查看内存相关命令03/25
- ♥ Windows 核心编程 _ 内核对象:线程同步二07/30
- ♥ X86_64汇编学习记述四08/09
- ♥ breakpad记述:Windows下静态库的编译使用03/15
- ♥ COM组件_207/22