回炉重造之重读Windows核心编程-007-线程的调度优先级与亲缘性

Posted leotsou

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回炉重造之重读Windows核心编程-007-线程的调度优先级与亲缘性相关的知识,希望对你有一定的参考价值。

  Windows被设计成一个抢占式的操作系统,用某种算法来确定哪些线程应该在何时被调度和运行多长时间。每隔20ms左右,Windows就要查看当前所有线程的内核对象,找到可以被调度的一个,将它加载到CPU寄存器中。这个操作成为上下文切换。Windows实际上保存了一个记录,说明每个线程获得了多少次运行的机会。使用MicrosoftSpy++这个工具可以了解这个情况。

  一个线程随时可以停止运行,一个线程可以进行调度。可以对线程进行一定程度的控制,但是不能太多。不能保证一个线程做任何事。

7.1暂停和恢复线程的运行  

  线程内核对象的内部有一个值,用以指明线程的暂停计数。当调用CreateThread创建线程内核对象的时候,暂停初始化为1。这可以防止线程被调度到CPU中,让系统有机会做好充分的准备再执行线程。

  初始化完毕,CreateThread或者CreateProcess就要查看是否传递了CREATE_SUSPEND标志,如果有这个标志函数就返回,同时新线程处于暂停状态;如果没有这个标志,那么函数就将暂停计数递减为0,让线程处于可调度的状态。

7.2暂停和恢复进程的运行  

   对于WIndows来说,不存在暂停或恢复进程的概念,因为进程从来不会获得CPU时间。但是线程是如何被暂停的呢?Windows确实允许一个进程暂停另一个进程的所有线程的运行,但是从事暂停操作的进程必须是个调试程序,必须是调用WaitForDebugEvent和ContinueDebugEvent之类的函数。

  Windows本身是一个抢占式的操作系统,没有提供其他方法来暂停进程中所有线程的执行。

  虽然无法创建完美的SuspendProcess函数,但是可以创建一个该函数的实现代码,在许多条件下出色地运行:

VOID SupendProcess(DWORD dwProcessID, BOOL fSuspend) {
  // Get the list of thread in the process 
  HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessID);
  if (hSnapShot != INVALID_HANDLE_VALUE){
    // Walk the list of thread 
    THREADENTRY32 te = {sizeof(te)};
    BOOL fOk = Thread32First(hSnapShot, &te);
    while (fOk) {
      // is this the thread in the desire process 
      if (te.th32OwnerProcessID == dwProcessID) {
        // convert id to handle
        HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);
        if (NULL != hThread) {
          // resume of suspend 
          if (fSuspend)
            SuspendThread(hThread);
          else
            ResumeThread(hThread);
        }
        CloseHandle(hThread);
      }
    }
    CloseHandle(hSnapShot);
  }
}

  使用的OpenThread函数能够找出带有匹配线程ID的线程内核对象,递增内核对象的引用计数,返回这个句柄。运用这个句柄,就可以调用ResumeThread和SuspendThread了。

  SuspendProcess不一定总是能运行,原因是线程可以随时被创建和撤销,如果线程已经被撤销,调用CreateToolHelp32Snapshot不能保证找到所有线程;或者在调用函数后创建了一个线程。更坏的情况是枚举线程ID的时候,相继被撤销和创建的两个线程如果拥有同样的ID,这将会导致其中任意一个线程的运行。

7.3睡眠方式  

   当然线程也可以想要在某个时间段内不被调度,通过Sleep函数实现。

VOID Sleep(DWORD dwMillisecond);

  这个函数可以暂停自己的运行,直到dwMillisecond那么多的时间结束。需要注意的是:

  1. Sleep函数可以让当前的线程放弃自己的时间片。
  2. 系统将在大约指定的毫秒数内使得线程不可调度,但这不意味着过了指定时间线程一定被唤醒;
  3. 如果给dwMillisecond参数传递INFINITE,系统将不会再调度这个线程。这不是个好事,会让资源没有办法释放。
  4. 如果给函数传递0,这将告诉系统当前线程将放弃当前的时间片,让系统有机会调度其他的线程。当然没有其他线程的话就重新调度自己。

7.4转换到另一个线程  

  系统提供了另一个函数使得另一个可调度程序能够运行:

BOOL SwitchToThread(();

  当调用这个函数的时候:

  • 系统要查看是否穿在一个迫切需要CPU时间的线程
    • 如果没有线程需要时间片,函数就会立即返回
    • 如果有线程需要时间片,函数就会调度该线程。

  这个函数允许一个需要资源的线程强制另一个优先级较低、目前却拥有资源的线程放弃该资源。

  和Sleep和相似。


7.5线程的运行时间 

  有的时候想要计算一个线程执行一个任务需要多长的时间,常见的方法是这样:

DWORD dwStartTime = GetTickCount();
// Perform complex algorithm here

// Subtract start time from current time to get duration
DWORD dwElapsedTime = GetTickCount - dwStart;

  这个方法有个银行的假设:当前进程不会被中断。但是在抢占式系统中,永远不能保证一个线程何时被调度。我们需要一个函数,用来计算线程运行的CPU时间的数量。幸运的是,Windows确实提供了这样的一个函数:

BOOL GetThreadTimes(
   HANDLE hThread,
   PFILETIME  pftCreationTime;
   PFILETIME  pftExitTime;
   PFILETIME  pftKernelTime;
   PFILETIME  pftUserTime
);

  通过这个函数,就可以用下面的代码确定复杂算法需要的时间量了。

__int64 FileTimeToQuadWord(PFILETIME pft) {
  return (Int64ShllMod32(pft->dwHighDateTime, 32) | pft->dwLowDateTime);
}
void PerformLongOperation() {
  FILETIME ftKernelTimeStart, ftKernelTimeEnd;
  FILETIME ftUserTimeStart, ftUserTimeEnd;
  FILETIME ftDummy;
  __int64 dwKernelTimeElapsed, dwUserTimeElapsed, qwTotalTimeElapsed;
  // Get Start Time 
  GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy,
    &ftKernelTimeStart, &ftKernelTimeEnd);
  // Perform complex algorithm
  // Get End Time
  GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy,
    &ftKernelTimeStart, &ftKernelTimeEnd);
  dwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) -
    FileTimeToQuadWord(&ftKernelTimeStart);
  dwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) -
    FileTimeToQuadWord(&ftUserTimeStart);
  // Get the time duration by the kernel and user
  qwTotalTimeElapsed = dwUserTimeElapsed + dwKernelTimeElapsed;
}

  GetProcessTimes是个类似的函数,使用与进程中的所有线程:

BOOL GetProcessTimes(
    HANDLE hProcess,
    PFILETIME pftCreationTime,
    PFILETIME pftExitTime,
    PFILETIME pftKernelTime,
    PFILETIME pftUserTime
)

  这个函数返回的时间适用于某个进程中的所有线程(包括已经终止的)。

  对于高分辨率的配置文件来说,GetThreadTimes并不完美,Windows提供了另一些特地用于高分辨率性能的函数:

BOOL QueryPerformanceFrequency(LARGE* pliFrequency);
BOOL QueryPerformanceCounter(LARGE* pliCount);

7.6运用环境结构

    环境结构的重要性现在已经不言而喻了,没有它,当然下次线程被调度的时候,就找不到它上次中断的地方。这样底层的数据结构一般不会完整地记录在PlatformSDK文档中。只是文档并没有说明结构的成员,也没有描述成员是谁,毕竟这些取决于系统运行在哪个CPU上。

 

   这个结构CONTEXT可以分为若干个部分。CONTEXT_CONTROL包含CPU的控制寄存器,CONTEXT_INTERGER用于标识CPU的整数寄存器。CONTEXT_FLOATING_POINT用于标识浮点寄存器,CONTEXT_SEGEMENT用于表示CPU的段寄存器,CONTEXT_DEBUG_REGISTER用于表示调试寄存器,CONTEXT_EXTENDED_REGISTER用于标识扩展寄存器。

  要获取这个结构,只需要调用GetThreadContex函数:

BOOL GetThreadContext(
    HANDLE hThread;
   PCONTEXT pContext 
);

  在调用这个函数之前,应该调用SuspendThread,否则线程可能被调度,而且线程的环境可能与你回收的不同。线程其实有两个环境,一个是用户方式,一个是内核方式。GetThreadContext只能返回用户方式环境,如果调用它用来停止线程的运行,而线程又在内核方式运行,那么即使SuspendThread尚未暂停该线程的运行,他的用户方式仍然处于稳定状态。线程在恢复用户方式前,无法执行更多的用户方式代码,因此可以放心地把线程视为处于暂停状态,GetThreadContext可以正常运行。

  ContextFlags成员用于指明你想用函数GetThreadContext获取哪些寄存器。只是注意要初始化ContextFlags。

  GetThreadContext和SetThreadContext函数使你能够对线程进行多方面的控制,只是使用的时候要小心。实际上几乎没有应用程序调用这些函数,增加它们只是为了增强调试程序和其他工具的功能,任何程序都可以使用它们。

 

7.7线程的优先级 

  为数众多的线程被赋予许多不同的优先级,这影响到调度程序将哪个线程取出来作为下个要运行的线程。

  每个线程都会被赋予一个从0(最低)到31(最高)的优先级号码。这样系统就可以以这个数字从大到小的顺序调度线程。系统会尽量使得计算机保持繁忙的状态。

  那是不是低优先级的线程就永远得不到机会运行了呢?不过在任何一个时段内,系统中的大多数线程是不能调度的,其他空闲线程就有机会被调度。

   假如高优先级线程抢在低优先级线程之前运行,不管低优先级线程正在运行什么。

  还有当系统引导时,它会创建一个特殊的线程,称为0页线程。它的优先级被设置为0,是整个系统中唯一的在优先级0上运行的线程。当系统没有任何线程需要执行操作时,0页线程负责将系统中的所有空闲RAM页面置0。

7.8对优先级的抽象说明

  线程的调度算法对用户运行的应用程序类型有相当大的影响,随着系统用途的变化,调度的算法也不断改变,而Windows又要保证软件在将来的系统版本上运行。那么Microsoft如何改变系统的工作方式而仍然抱着软件能够运行呢?下面是一下方法:

  • 没有将调度程序的行为特性完全固定下来。
  • 没有让应用程序充分利用调度程序的特性。
  • 声称调度程序的算法是变化的,在编写代码时应有所准备。

  因此WindowsAPI展示了系统调度程序上的一个抽象层,这样就永远不会直接与调度程序通信。Windows支持6个优先级类:空闲、低于正常、正常、高于正常、高和实时。当然, 正常优先级是最常用的,99%的应用程序使用它。

  当系统什么都不做的时候,使用空闲优先级是最合适不过的了。只有在绝对必要的时候,才可以使用高优先级,例如Windows Explorer就在按组合键或者操作的时候被唤醒,否则它就处于暂停状态,大部分情况下都会抢在其他线程前运行。

  应该避免使用实时优先级类。如果使用它很有可能会干扰操作系统的运行,毕竟大多数操作系统线程均以较低的优先级在运行,必要的磁盘IO和网络信息的产生可能会别阻止。还有鼠标和键盘的输入不会得到及时的处理。

  一旦选定了优先级类别后,就不必考虑应用程序之间的关系了,只需要集中考虑你的应用程序中的各个线程。Windows支持7个相对的线程优先级:空闲、最低、低于正常、正常、高于正常、最高和关键时间优先级。这些优先级是相对于进程的优先级类而言的,大多数线程都使用正常线程优先级。

  开发人员不必具体设置优先级,相反,系统负责将线程的优先级类和进程的优先级类映射到一个优先级上。

7.9程序的优先级 

  进程是如何被赋予优先级类的呢?当调用CreateProcess时,可以在fdwCreate参数中传递需要的优先级类:

  1. 实时:REALTIME_PRIORITY_CLASS
  2. 高:HIGH_PRIORITY_CLASS
  3. 高于正常:ABOVE_NORMAL_PRIORITY_CLASS
  4. 正常:NORMAL_PRIORITY_CLASS
  5. 低于正常:BELOW_PRIORITY_CLASS
  6. 空闲:IDLE_PRIORITY_CLASS

  创建子进程的进程负责选择子进程运行的优先级别,这看起来有点奇怪。可是一旦子进程被创建完毕,就能用SetPriorityClass函数改变自己的优先级:

BOOL SetPriorityClass(
   HANDLE hProcess,
   DWORD fdwProirity);

  fdwProirity参数可以在进程优先级的类别中选择。再加上hProcess的访问权限,就能改变任何进程的优先级类。把自己变成空闲线程的方式:

SetPRiorityClass(
    GetCurrentProcess(),
    IDLE_PRIORITY_CLASS);

  获取进程的优先级GetPriorityClass,返回进程的优先级类别。

  如果使用命令外壳启动程序,程序的优先级是正常优先级。但是如果使用start命令启动程序,就是空闲优先级别了,除非你不使用/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGH和/REALTIME等开关。当然还可以用SetPriorityClass设定优先级,Windows的任务管理器就有这个功能。技术图片

   当线程被创建的时候,它的优先级总是被设置为正常优先级。奇怪的是,CreateThread没有为调用者提供 一个设置新线程的相对优先级的方法,只有设置优先级的。

BOOL SetThreadPriority(
    HANDLE hThread,
    int nPriority
);
int GetThreadPriority(
  
HANDLE hThread
);

  优先级别也和进程的类别大致相同。还有就是检索线程的相对优先级别的补充函数GetThreadPriority。如果要创建优先级为空闲的线程,可以执行以下的代码:

DWORD dwThreadID;
HANDLE hThread = CreateThread(
    NULL, 0, ThreadFunc, NULL, CREATE_SUSPEND, &dwThreadID
);
ResumeThread(hThread);
CloseHandle(hThread);

7.9.1动态提高线程的优先级等级 

  综合考虑进程和线程的优先级后,系统就可以确定线程的优先级别了,这也称为基本优先等级。假定一个线程的基本优先级别是13,然后某种操作导致13升级到了15;当操作结束后,优先级就从15将为14了。下一个时间片按照13来执行。

  现在看来系统动态提高线程优先级的功能对他们的线程性能会产生一种不良的影响,为此Windows增加了以下就个函数,使得系统的动态提高线程有限等级的功能失效了:

BOOL SetProcessPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost   
;
BOOL SetThreadPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost   
;

BOOL GetProcessPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost   
;
BOOL GetThreadPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost   
;

 

7.9.2为前台进程调整调度程序 

  如果需要前台进程比后台线程有更强的相应能力,Windows就要为前台进程中的线程调整其调度算法。

7.9.3SchedulingLab示例应用程序 

  见代码清单。

7.10亲缘性 

   按照默认设置,当系统将线程分配给处理器时,Windows使用软亲缘性进行操作。让现场留在单个处理器上,有助于重复使用仍然在处理器的内存高速缓存中的数据。

  系统在引导时操作系统需要确定有多少个CPU可以是用,通过调用GetSystemInfo函数,应用程序就能查询到机器中的CPU数量按照默认的设置,任何线程都可以调度到这些CPU中的任意一个上去运行。为了限制在可用CPU的子集上的运行当个进程的线程数量,可以使用SetProcessAffinityMask:

BOOL SetProcessAffinityMask(
    HANDLE hProcess,
    DWORD_PTR dwProcessAffinityMask
);

  第一个参数是进程句柄,第二个参数是个位屏蔽,用以指明进程在那个CPU上运行。

  子进程可以继承进程的亲缘性。

  当然,还有一个函数可以返回位屏蔽,那就是GetProcessAffinityMask,它还能返回系统的亲缘性位屏蔽。

以上是关于回炉重造之重读Windows核心编程-007-线程的调度优先级与亲缘性的主要内容,如果未能解决你的问题,请参考以下文章

回炉重造之重读Windows核心编程-011-线程池和其他异步方式

回炉重造之重读Windows核心编程-026- 窗口消息

回炉重造之重读Windows核心编程-014-虚拟内存

回炉重造之重读Windows核心编程-001-错误处理

回炉重造之重读Windows核心编程-003-内核对象

回炉重造之 nginx