[笔记]Windows核心编程《六》线程调度、优先级和关联性

系列文章目录

[笔记]Windows核心编程《一》错误处理、字符编码
[笔记]Windows核心编程《二》内核对象
[笔记]Windows核心编程《三》进程
[笔记]Windows核心编程《四》作业
[笔记]Windows核心编程《五》线程基础
[笔记]Windows核心编程《六》线程调度、优先级和关联性
[笔记]Windows核心编程《七》用户模式下的线程同步
[笔记]Windows核心编程《八》用内核对象进行线程同步
[笔记]Windows核心编程《九》同步设备I/O和异步设备I/O
[笔记]Windows核心编程《十一》Windows线程池
[笔记]Windows核心编程《十二》纤程
[笔记]Windows核心编程《十三》windows内存体系结构
[笔记]Windows核心编程《十四》探索虚拟内存
[笔记]Windows核心编程《十五》在应用程序中使用虚拟内存
[笔记]Windows核心编程《十六》线程栈
[笔记]Windows核心编程《十七》内存映射文件
[笔记]Windows核心编程《十八》堆栈
[笔记]Windows核心编程《十九》DLL基础
[笔记]Windows核心编程《二十》DLL的高级操作技术
[笔记]Windows核心编程《二十一》线程本地存储器TLS
[笔记]Windows核心编程《二十二》注入DLL和拦截API
[笔记]Windows核心编程《二十三》结构化异常处理

相关:
参考1
参考2

前言

每个线程都有一个CONTEXT结构,保存在线程内核对象中。
大约每隔20ms windows就会查看所有当前存在的线程内核对象。并在可调度的线程内核对象中选择一个,将其保存在CONTEXT结构的值载入cpu寄存器。这被称为上下文切换。大约又过20ms windows将当前cpu寄存器存回内核对象,线程被挂起。Windows再次检查内核对象,并在可调度的内核对象中选择一个进行调度。此过程不断重复直到系统关闭。

线程的挂起和恢复

在线程内核对象中有一个值表示线程的挂起计数。调用CreateProcess或者 CreateThread时,系统将创建线程内核对象,并把挂起计数初始化为1。这样,就不会给这个线程调度CPU了。这正是我们所希望的,因为线程初始化需要时间,我们当然不想在线程准备好之前就开始执行它。

在线程初始化之后,CreateProcess 或者Createrhread 函数将查看是否有CREATE_SUSPENDED标志传入。如果有,函数会返回并让新的线程处于挂起状态。如果没有,函数会将线程的挂起计数递减为0。当线程的挂起计数为0时,线程就成为可调度的了,除非它还在等待某个事件发生(例如键盘输入)。

通过创建一个处于挂起状态的线程,我们可以在线程执行任何代码之前改变它的环境(比如本章稍后将讨论的优先级)。改变了线程的环境之后,必须使其变为可调度的。这可以通过调用ResumeThread 函数,传入调用CreateThread时所返回的线程句柄(或者传给CreateProcess的ppiProclnfo参数所指向的结构中的线程句柄)予以实现:

DWORD ResumeThread(
  HANDLE hThread
);

如果Resumerhread函数成功,它将返回线程的前一个挂起计数;否则,它将返回0XFFFFFFFF。

一个线程可以被多次挂起。如果一个线程被挂起三次,则在它有资格让系统为它分配cPU之前必须恢复三次。除了在创建线程时使用 CREATE_SUSPENDED标志外,还可以通过调用SuspendThread来挂起线程:

DWORD SuspendThread(
  HANDLE hThread
);

任何线程都可以调用这个函数挂起另一个线程(只要有线程的句柄)。显然,线程可以将自挂起,但是它无法自己恢复。与ResumeThread 一样,SuspendrThread 返回线程之前的挂起计数。一个线程最多可以挂起MAXIMUM SUSPEND_COUNT(WinNT.h中定义为127次。请注意,就内核模式下面执行情况而言,SuspendThread 是异步的,但在线程恢复之前,它是无法在用户模式下执行的。

实际开发中,应用程序在调用SuspendThread时必须小心,因为试图挂起一个线程时,我们不知道线程在做什么。例如,如果线程正在分配堆中的内存,线程将锁定堆。当其他线程要访问堆的时候,它们的执行将被中止,直到第一个线程恢复。只有在确切知道目标线程是哪个(或者它在做什么),而且采取完备措施避免出现因挂起线程而引起的问题或者死锁的时候,调用SuspendThread才是安全的。

进程的挂起和恢复

其实,Windows中不存在挂起和恢复进程的概念,因为系统从来不会给进程调度CPU时间。在一个特殊情况下,即调试器处理WaitForDebugEvet返回的调试事件时,Windows将冻结被调试进程中的所有线程,直到调试器调用ContinueDebugEvent。

Windows没有提供其他方式挂起进程中的所有线程,因为存在竞态条件问题。例如,在线程被挂起时,可能创建一个新的进程。系统必须想方设法挂起这个时间段中任何新的线程。

SuspendThread

睡眠

线程还可以告诉系统,在一段时间内自己不需要调度了。这可以通过调用Sleep实现:

void Sleep(
  DWORD dwMilliseconds
);

这个函数将使线程自己挂起dw/Millseconds长的时间。
关于Sleep,有以下几点重要的事项需要注意:

  1. 调用Sleep函数,将使线程自愿放弃属于它的时间片中剩下的部分
  2. 系统设置线程不可调度的时间只是“近似于”所设定的毫秒数。没错,如果告诉系统想睡眠100ms,那么线程将睡眠差不多这么长时间,但是可能会长达数秒甚至数分钟。别忘了,Windows不是实时操作系统。我们的线程可能准时醒来,但是实际情况取决于系统中其他线程的运行情况。
  3. 可以调用Sleep并给dwMs参数传入INFINITE。这是在告诉系统,永远不要调度这个进程。这样做没有什么用处。让线程退出并将其栈和内核对象返还给系统,要好得多。
  4. 可以给Sleep传入0。这是在告诉系统,主调线程放弃了时间片的剩余部分,它强制系统调度其他线程。但是系统有可能重新调度刚刚调用了Sleep的那个线程。如果没有相同或者较高优先级的可调度线程时,就会发生这样的事情。

切换到另一个线程

BOOL SwitchToThread();

调用这个函数时,系统查看是否存在正急需CPU时间的饥饿线程。如果没有,SwitchToThread 立即返回。如果存在,SwitchToThread将调度该线程(其优先级可能比SwitchToThread的主调线程低)。饥饿线程可以运行一个时间量,然后系统调度程序恢复正常运行。

通过这个函数,需要某个资源的线程可以强制一个可能拥有该资源的低优先级的线程放弃资源。如果在调用SwitchToThread时没有其他线程可以运行,则函数将返回FALSE;否则,函数将返回一个非零值。

Sleep和SwitchToThread对比

  • Sleep(0):时间片只能让给优先级相同或更高的线程,
  • SwitchToThread():只要有可调度线程,即便优先级较低,也会让其调度。

在线程没退出之前,线程有三个状态,就绪态,运行态,等待态。sleep(n)之所以在n秒内不会参与CPU竞争,是因为当线程调用sleep(n)的时候,线程是由运行态转入等待态,线程被放入等待队列中,等待定时器n秒后的中断事件,当到达n秒计时后,线程才重新由等待态转入就绪态,被放入就绪队列中,等待队列中的线程是不参与cpu竞争的,只有就绪队列中的线程才会参与cpu竞争,所谓的cpu调度,就是根据一定的算法(优先级,FIFO等),从就绪队列中选择一个线程来分配cpu时间。而sleep(0)之所以马上回去参与cpu竞争,是因为调用sleep(0)后,线程直接回到就绪队列,而非进入等待队列,只要进入就绪队列,那么它就参与cpu竞争。

在超线程CPU上切换另一个线程

线程的执行时间

有时候,我们需要计算一个线程执行某项任务需要消耗多长时间。关于这一点,许多人的做法是编写如下代码,代码中利用了新的GetTickCount64函数:

ULONGLONG GetTickCount64();

在实际上下文中谈Context结构

线程优先级

在调度程序给一个可调度线程分配CPU之前,CPU可以运行一个线程大约20ms。这是优先级都相同的情况,实际上,各个线程有很多不同的优先级,这将影响调度程序如何选择下一个要运行的线程。

1)每个线程被赋予0(最低)~31(最高)的优先级数。

2)CPU首先查看优先级最高的线程,并以循环(round-robin)的方式进行调度。一个31优先级的结束,cpu会调度领一个优先级为31的线程。

3)只要有优先级为31的线程,系统就不会给优先级0-30的线程分配CPU,称为饥饿(starvation)在多处理器系统上饥饿发生的可能性要小得多。

4)较高优先级的线程会抢占较低优先级线程的时间片,例如一个优先级的5线程正在执行,系统确定有一个更高优先级的线程准备运行,会立即暂停较低优先级的线程(即使他还有时间片没用完)并将cpu分配给较高优先级的线程,该线程将获得一个完整的时间片。

系统启动时会创建一个0优先级的页面清0线程(zero page thread)负责在系统空闲时将内地中所有闲置页面清零。

创建子进程的进程会选择子进程运行的优先级,这听起来有些奇怪。举个例子,让我们考虑Windows资源管理器。使用Windows资源管理器运行一个程序时,新的进程将运行在normal优先级。Windows资源管理器并不知道进程在做什么,或者它的线程多久会被调度一次。但是,一旦进程运行,便可以通过调用SetPriorityClass来改变自己的优先级:

BOOL SetPriorityClass(
  HANDLE hProcess,
  DWORD  dwPriorityClass
);

这个函数将hProcess表示的优先级修改为参数fdwPriority所指定的值。fdwPriority参数可以是表7-5中的任何一个标识符。因为该函数有一个参数是进程句柄,所以只要有它的句柄和足够的访问权限,我们就可以改变系统中的任何进程的优先级。

用来获取进程优先级的相应函数如下:

DWORD GetPriorityClass(
  HANDLE hProcess
);

从抽象角度看优先级

优先级编程

动态提升线程优先级

为前台进程微调调度程序

调用I/O请求优先级

当线程有io事件或消息到来时,操作系统会暂时提高线程的优先级;或者线程可调度但长时间(数秒)都得不到时间片的时候,系统开发也会暂时提高线程优先级。可以设置是否允许系统开发自动提升优先级:setprocesspriorityboost、setthreadpriorityboost。

Scheduling Lab示例程序

关联性

默认情况下,windows在分配cpu时采用软关联的方式。也就是说在其他因素相同的情况下,系统使线程在上一次运行的处理器上运行。这有助于重用仍在处理器高速缓存中的数据。

系统在启动时确定cpu数量。应用程序可以通过调用GetSysInfo来查询cpu的数量。如果需要限制一个进程的所有线程在某些cpu上运行,可以调用:

BOOL SetProcessAffinityMask(
         HANDLE hProcess,
         DWORD_PTR dwProcessAffinityMask);

第一个参数 hProcess:代表要设置的进程句柄。

第二参数 dwProcessAffinityMask:是一个位掩码。代表线程可以在哪些cpu上运行。

注意子进程将继承父进程的关联性。

GetProcessAffinityMask返回进程的关联掩码。

相应的还可以设置某个线程只在一组cpu上运行:

SetThreadAffinityMask

有时候强制一个线程只在某个特定的cpu上运行并不是什么好主意。
Windows允许一个线程运行在一个cpu上,但如果需要,它将被移动到一个空闲的cpu上。实际即使不用此函数,操作系统会自主将其分配到两个空闲CPU上。
要给线程设置一个理想的cpu,可以调用:

DWORD SetThreadIdealProcessro(
      HANDLE hThread
      DWORD dwIdealProcessor);
 

dwIdealProcessor是一个0到31/63之间的整数。表示线程希望设置的cpu。可以传入MAXIMUM_PROCESSOR值,表示没有理想的cpu。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二进制怪兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值