Windows C/C++ 学习线程调度、线程优先级和亲缘性

来源:互联网 发布:淘宝买家v4算星级高吗 编辑:程序博客网 时间:2024/05/16 10:08

每一个线程在它的线程内核对象中有一个上下文结构,反映了线程最后一次执行的 CPU 寄存器状态,每隔大约 20ms(可以使用 GetSystemTimeAdjustment 获得,我测得这个间隔大约为 15.6ms),Windows 在所有当前内核对象中查找可调度线程,并选择一个可调度线程,从这个线程的上下文结构中读取 CPU 寄存器状态,这个动作叫做“上下文切换”。这时,这个线程开始执行它的代码,大约 20ms 后,Windows 将 CPU 寄存器的状态保存到这个线程的上下文结构中,这个线程不再执行,Windows 查找下一个可调度线程执行上下文切换,另一个线程继续从它上一次中断的地方开始执行,这个过程从系统开机到系统关闭周而复始。

某些线程不是可调度线程,因为这些线程可能被暂停执行,或者正在待某个任务完成。

由于 WIndows 系统是抢先式操作系统,意味着一个线程可能会在任何时候被其它线程中继,又因为 Windows 系统不是实时操作系统,所以不能保证某一时刻某个线程一定会被调度执行,也不能保证这个线程一定会执行给定的时间片。

线程的暂停和恢复

在线程内核对象内部有一个表示暂停计数器的成员,当你调用 CreateProcess 或 CreateThread 时,这个计数器初始化为 1,避免线程被调度给 CPU,保证线程在完全初始化之前不会执行任何代码,一旦线程完全初始完成,创建函数会检测你是否传递了一个 CREATE_SUSPENDED 标志,如果没有传递这个标志,函数会减少暂停计数器计数,只要计数器为 0,这个线程就成为可调度线程,否则它会暂停执行。

可以使用

?
1
DWORD ResumeThread(HANDLE hThread);

来恢复线程的执行,这个函数如果成功,返回之前线程被暂停的次数,否则返回 0xFFFFFFFF。

一个线程可以被暂停多次,那么恢复的话也必须调用 ResumeThread 多次,任何线程都可以调用

?
1
DWORD SuspendThread(HANDLE hThread);

来暂停一个线程的执行,只要它有那个线程的句柄,一个线程可以暂停自身的执行,但不可恢复自身。一个线程可以最多被暂停 MAXIMUM_SUSPEND_COUNT (WinNT.h 定义为 127 )次。暂停一个线程时必须非常注意,因为你不知道将要被暂停的线程正在做什么,比如,如果一个线程正在从堆中申请内存,这时,这个线程会在堆中有一个锁,如果这时线程被暂停执行,其它线程会一直待锁被释放,这样会造成死锁。

暂停和恢复一个进程

Windows 并没有提供一个暂停和恢复一个进程内所有线程的函数,我们可以枚举一个进程内的所有线程来达到这个目的,下面是一段摘自 Windows via C/C++ 的一段代码:

?
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
26
27
28
29
30
VOID SuspendProcess(DWORD dwProcessId, BOOL fSuspend)
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessId);
  
    if(hSnapshot != INVALID_HANDLE_VALUE)
    {
        THREADENTRY32 te = {sizeof(THREADENTRY32)};
        BOOL fOk = Thread32First(hSnapshot, &te);
        for(; fOk; fOk = Thread32Next(hSnapshot, &te))
        {
            // 当前线程属于进程吗?
            if(te.th32OwnerProcessID == dwProcessId)
            {
                // 将线程ID转换为一个句柄
                HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME,
                    FALSE, te.th32OwnerProcessID);
                if(hThread != NULL)
                {
                    // 暂停或恢复这个线程
                    if(fSuspend)
                        SuspendThread(hThread);
                    else
                        ResumeThread(hThread);
                }
                CloseHandle(hThread);
            }
        }
        CloseHandle(hSnapshot);
    }
}

但是这段代码可能不会 100% 有效,如果在调用 CreateToolhelp32Snapshot 后目标进程创建了一个新线程,那么这个线程不会被枚举到,新线程也就不会被暂停;更糟糕的是,在枚举一个线程的过程中一个目标进程的线程可能被释放另一个线程正在被创建,新线程的 ID 可能与释放的线程 ID 相同但可能不属于同一个进程,这样有可能造成暂停了另一个进程的线程。

睡眠

一个线程可以调用

?
1
VOID Sleep(DWORD dwMilliseconds);

来使自己睡眠一段时间,这段时间内线程不可调度。

调用这个函数会使线程自动放弃已分配给它的剩余时间片,睡眠的时间也是一个近似值,如果传递一个 INFINITE 参数,表示线程会一直睡眠,直到进程终止,一般情况下很少这样用,但一个永不唤醒的线程可以保留它的线程栈和线程内核对象,其它线程可以使用它们,如果传递一个 0 参数,它告诉线程放弃剩余的时间片,强制系统调度另一个线程,可是如果没有其它线程可调度的话,这个线程可能会立即被重新调度执行。

CONTEXT 结构

CONTEXT 结构允许系统记住线程的状态,线程下一次得到 CPU 时间的时候它可以继续执行。这个结构在 WinNT.h 中定义,它的每一个成员对应一个 CPU 寄存器。

CONTEXT 结构有几个部分,CONTEXT_CONTROL 包含了 CPU 的控制寄存器,如指令指针、栈指针、标志寄存器、函数返回地址等;CONTEXT_INTEGER 表示 CPU 的整数寄存器;CONTEXT_FLOATING_POINT 表示 CPU 的浮点指针寄存器;CONTEXT_SEGMENTS 表示 CPU 的段寄存器;CONTEXT_DEBUG_RGEISTERS 表示 CPU 的调试寄存器;CONTEXT_EXTENDED_REGISTERS 表示 CPU 的扩展寄存器。

可以使用

?
1
2
3
BOOL GetThreadContext(
    HANDLE      hThread,
    PCONTEXT    pContext);

来获取 CONTEXT 结构,调用这个函数之前,你必须为 CONTEXT 分配内存空间,并且设置 ContextFlags 来确定你期望得到哪些寄存状态,你可以在调用 GetThreadContext 之前调用 SuspendThread 来暂停线程的执行,否则你可能得不到指定点的寄存器值,由于 SuspendThread 函数只会暂停用户模式下代码的执行,不会对内核模式代码有影响,所以可以放心地调用 GetThreadContext,它会在内核模式下继续运行来获得期望的结果。

可以使用

?
1
2
3
BOOL SetThreadContext(
    HANDLE      hThread,
    CONST CONTEXT   *pContext);

来设置寄存器的内容,这是一个将线程搞崩溃的好方法。

线程优先级

Windows 系统有 32 种优先级,分别是 0(最低)到 31(最高)。Windows 是一种抢先式操作系统,意味着高优先级的线程会抢先低优先级的线程的执行,只要有高优先级的线程正在执行,低优先级的线程就不会有机会被调度。

Windows 系统并不直接对线程设置优先级,而是使用进程优先级类和线程相对优先级来设置一个线程的优先级。有多各种方法设置优先级类,可以使用 CreateProcess 创建子进程时传递给 fdwCreate 一个表示优先级类的标识符进行设置,还可以通过

?
1
2
3
BOOL SetPriorityClass(
    HANDLE  hProcess,
    DWORD   fdwPriority);

设置指定进程的优先级类,第三种方式是使用 start 命令行并指定的一个表示优先级选项来启动一个进程,还可以在任务管理器中设置一个进程的优先级。有 6 种进程优先级类,分别是

优先级类标识符实时REALTIME_PRIORITY_CLASS高HIGH_PRIORITY_CLASS高于默认ABOVE_NORMAL_PRIORITY_CLASS默认NROMAL_PRIORITY_CLASS低于默认BELOW_NORMAL_PRIORITY_CLASS空闲IDLE_PRIORITY_CLASS

不能在 CreateThread 时设置新线程的相对优先级,只能通过

?
1
2
3
BOOL SetThreadPriority(
    HANDLE hThread,
    intnPriority);

设置一个指定线程的优先级,有 7 个相对优先级,分别是

相对优先级常量标识符时间关键THREAD_PRIORITY_TIME_CRITICAL最高THREAD_PRIORITY_HIGHEST高于默认THREAD_PRIORITY_ABOVE_NORMAL默认THREAD_PRIORITY_NORMAL低于默认THREAD_PRIORITY_BELOW_NORMAL最低THREAD_PRIORITY_LOWEST空闲THREAD_PRIORITY_IDLE

线程优先级的动态提升

通常系统在处理一些 I/O 事件或磁盘读取时会动态提升相应线程的优先级,例如,用户按下一个按键,系统会将一个 WM_KEYDOWN 消息放入线程的消息队列中,键盘驱动程序会告诉系统临时提升线程的优先级来处理这个消息,默认情况提升 2 个级别,在第二个时间片,它的优先级降低 1,第三个时间片降到平常水平。系统仅对 1 到 15 之间的优先级做这种提升,这之间的优先级叫做动态优先级范围,系统不会对高于 15 级以上的线程做动态提升,另外,这个提升是由驱动程序告诉系统的。可以使用

?
1
2
3
4
5
6
BOOL SetProcessPriorityBoost(
    HANDLE hProcess,
    BOOL   bDisablePriorityBoost);
BOOL SetThreadPriorityBoost(
    HANDLE hThread,
    BOOL   bDisablePriorityBoost);

分别设置是否将相应进程或相应线程进行动态提升,使用

?
1
2
3
4
5
6
BOOL GetProcessPriorityBoost(
    HANDLE hProcess,
    PBOOL  pbDisablePriorityBoost);
BOOL GetThreadPriorityBoost(
    HANDLE hThread,
    PBOOL  pbDisablePriorityBoost);

来获得相应进程或线程是否禁用了动态提升。

另外,如果一个低优先级的线程已准备好执行,可是因为高优先级的线程正在一直执行,这种情况如果持续一段时间,一般是 3 到 4 秒钟,系统会临时将低优先级的线程的优先级提高到 15,并运行两个时间片的时间,然后再将的优先级降到平常水平,这样能保证低优先级的线程能得到执行。

I/O 请求优先级调度

当一个线程正在进行长时间的 I/O 请求时,系统因为慢速的 I/O 操作导致响应不畅,解决这个问题的办法是在开始 I/O 操作之前传递 THREAD_MODE_BACKGROUND_BEGIN 标志给 SetThreadPriority,告诉系统降低当前线程的优先级,在 I/O 完成之后传递 THREAD_MODE_BACKGROUND_END 标志给 SetThreadPriority 将优先级恢复到之前的状态;还可以使用 SetProcessClass 分别传递 PROCESS_MODE_BACKGROUND_BEGIN 和 PROCESS_MODE_BACKGROUND_END 来降低或恢复当前进程中所有线程的优先级,注意,只能对当前进程或线程进行这样的操作,不允许这样改变其它进程和其它线程的优先级。



线程优先级SetThreadPriority的使用

SetThreadPriority  设置指定线程的优先级
[cpp]  
BOOL SetThreadPriority(  HANDLE hThread, // handle to the thread 
  int nPriority   // thread priority level); 
参数说明
  hThread 要设置的线程句柄
  nPriority 优先级别参数 可设置为一下参数
  THREAD_PRIORITY_ABOVE_NORMAL  比一般优先级高一个等级
  THREAD_PRIORITY_BELOW_NORMAL 比一般低一个等级
  THREAD_PRIORITY_HIGHEST                 比一般高2个等级(最高)
  THREAD_PRIORITY_IDLE                          空闲
  THREAD_PRIORITY_LOWEST                  比一般低2个等级(最低)
  THREAD_PRIORITY_NORMAL                  一般等级
  THREAD_PRIORITY_TIME_CRITICAL     实时

HANDLE threadHandle = GetCurrentThread();

  1. // ThreadPriority.cpp : 定义控制台应用程序的入口点。  
  2. //  
  3.   
  4. #include "stdafx.h"  
  5. #include <Windows.h>  
  6.   
  7. DWORD WINAPI ThreadProcIdle(LPVOID lpParameter)  
  8. {  
  9.     for (int i=0;i<20;i++)  
  10.     {  
  11.         printf("I'm in thread IDLE...\n");  
  12.     }  
  13.     return 0;  
  14. }  
  15.   
  16. DWORD WINAPI ThreadProcNormal(LPVOID lpParameter)  
  17. {  
  18.     for (int i=0;i<20;i++)  
  19.     {  
  20.         printf("I'm in thread Normal...\n");  
  21.     }  
  22.     return 0;  
  23. }  
  24.   
  25.   
  26. int _tmain(int argc, _TCHAR* argv[])  
  27. {  
  28.     DWORD dwThreadIdIdle;  
  29.     DWORD dwThreadIdNormal;  
  30.     HANDLE hThread[2];  
  31.     //开启两个线程  
  32.     hThread[0] = ::CreateThread(NULL,0, ThreadProcIdle, NULL, CREATE_SUSPENDED, &dwThreadIdIdle);  
  33.     ::SetThreadPriority(hThread[0],THREAD_PRIORITY_IDLE);  
  34.     ::ResumeThread(hThread[0]);  
  35.   
  36.     hThread[1] = ::CreateThread(NULL,0, ThreadProcNormal, NULL, CREATE_SUSPENDED, &dwThreadIdNormal);  
  37.     ::SetThreadPriority(hThread[1],THREAD_PRIORITY_NORMAL);  
  38.     ::ResumeThread(hThread[1]);  
  39.   
  40.     //等待两个线程结束  
  41.     ::WaitForMultipleObjects(2,hThread,TRUE,INFINITE);  
  42.     ::CloseHandle(hThread[0]);  
  43.     ::CloseHandle(hThread[1]);  
  44.     return 0;  
  45. }  

原创粉丝点击