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

简介: 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


文章目录



   系列文章目录

   前言

   线程的挂起和恢复

   进程的挂起和恢复

   睡眠

   切换到另一个线程

       Sleep和SwitchToThread对比

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

   线程的执行时间

   在实际上下文中谈Context结构

   线程优先级

   从抽象角度看优先级

   优先级编程

       动态提升线程优先级

       为前台进程微调调度程序

       调用I/O请求优先级

       Scheduling Lab示例程序

   关联性


前言


每个线程都有一个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
);


一个线程可以被多次挂起。如果一个线程被挂起三次,则在它有资格让系统为它分配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,有以下几点重要的事项需要注意:


调用Sleep函数,将使线程自愿放弃属于它的时间片中剩下的部分。

系统设置线程不可调度的时间只是“近似于”所设定的毫秒数。没错,如果告诉系统想睡眠100ms,那么线程将睡眠差不多这么长时间,但是可能会长达数秒甚至数分钟。别忘了,Windows不是实时操作系统。我们的线程可能准时醒来,但是实际情况取决于系统中其他线程的运行情况。

可以调用Sleep并给dwMs参数传入INFINITE。这是在告诉系统,永远不要调度这个进程。这样做没有什么用处。让线程退出并将其栈和内核对象返还给系统,要好得多。

可以给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天前
|
数据采集 存储 数据处理
Python中的多线程编程及其在数据处理中的应用
本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
|
5天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
14天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
11天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
14天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
14天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
42 1
|
18天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
19天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
19天前
|
消息中间件 供应链 Java
掌握Java多线程编程的艺术
【10月更文挑战第29天】 在当今软件开发领域,多线程编程已成为提升应用性能和响应速度的关键手段之一。本文旨在深入探讨Java多线程编程的核心技术、常见问题以及最佳实践,通过实际案例分析,帮助读者理解并掌握如何在Java应用中高效地使用多线程。不同于常规的技术总结,本文将结合作者多年的实践经验,以故事化的方式讲述多线程编程的魅力与挑战,旨在为读者提供一种全新的学习视角。
46 3
|
20天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
32 1