Windows 采用基于优先级的、抢占调度算法来调度线程。
用于处理调度的 Windows 内核部分称为调度程序,Windows 调度程序确保具有最高优先级的线程总是在运行的。由于调度程序选择运行的线程会一直运行,直到被更高优先级的线程所抢占,或终止,或时间片已到,或调用阻塞系统调用(如 I/O)。如果在低优先级线程运行时,更高优先级的实时线程变成就绪,那么低优先级线程就被抢占。这种抢占使得实时线程在需要使用 CPU 时优先得到使用。
调度程序采用 32 级的优先级方案,以便确定线程执行顺序。优先级分为两大类:可变类包括优先级从 1〜15 的线程,实时类包括优先级从 16〜31 的线程(还有一个线程运行在优先级 0,它用于内存管理)。
调度程序为每个调度优先级采用一个队列;从高到低检查队列,直到它发现一个线程可以执行。如果没有找到就绪线程,那么调度程序会执行一个称为空闲线程的特别线程。
在 Windows 内核和 Windows API 的数字优先级之间有一个关系。Windows API 定义了一个进程可能属于的一些优先级类型。它们包括:
进程通常属于类 NORMAL_PRIORITY_CLASS。进程属于这个类,除非进程的父进程属于类 IDLE_PRIORITY_CLASS,或者在创建进程时指定了某个类。此外,通过 Windows API 的函数 SetPriorityClass(),进程的优先级的类可以修改。除了 REALTIME_PRIORITY_CLASS外,所有其他类的优先级都是可变的,这意味着属于这些类型的线程优先级能够改变。
具有给定优先级类的一个线程也有一个相对优先级。这个相对优先级的值包括:
每个线程的优先级基于它所属的优先级类型和它在该类型中的相对优先级,图 2 说明了这种关系。
每个类型的值出现在顶行。左列包括相对优先级的值。例如,如果 1 个线程属于 ABOVE_NORMAL_PRIORITY_CLASS 型,且相对优先级为 NORMAL,那么该线程的优先级数值为 10。
另外,每个线程在所属类型中有一个优先级基值。默认地,优先级基值为一个类型的优先级相对值 NORMAL。每个优先级类型的优先级基值为:
线程的优先级初值通常为线程所属进程的优先级基值,但是通过 Windows API 的函数 SetThreadPriority() 也可修改线程的优先级基值。
当一个线程的时间片用完时,该线程被中断。如果线程属于可变的优先级类型,那么它的优先级就被降低。不过,该优先级不能低于优先级基值。降低优先级可以限制计算密集型线程的 CPU 消耗。
当一个可变优先级的线程从等待中释放时,调度程序会提升其优先级。提升数量取决于线程等待什么。例如,等待键盘 I/O 的线程将得到一个较大提升;而等待磁盘操作的线程将得到一个中等提升。采用这种策略,正在使用鼠标和窗口的线程往往得到很好的响应时间。这也使得 I/O 密集型线程保持 I/O 设备忙碌,同时允许计算密集型线程使用后台空闲的 CPU 周期。
此外,用户正在交互使用的窗口会得到优先级提升,以便改善响应时间。多个操作系统包括 UNIX,采用这种策略。
当用户运行一个交互程序时,系统需要提供特别好的表现。由于这个原因,对于类 NORMAL PRIORITY CLASS 的进程,Windows 有一个特殊调度规则。Windows 将这类进程分成两种:
当一个进程移到前台,Windows 增加它的时间片,通常是原来的 3 倍。这个增加给前台进程 3 倍的时间来运行(在被抢占前)。
Windows 7 引入用户模式调度(UMS),允许应用程序在内核外创建和管理线程。因此,一个应用程序在不涉及内核调度程序的情况下可以创建和调度多个线程。对于创建大量线程的应用程序,用户模式的线程调度比内核模式更加有效,因为不需要内核的干预。
早期版本的 Windows 提供了一个类似特征(称为纤程),允许多个用户模式线程(纤程)被映射到单个内核线程。然而,使用纤程实际上有限制。一个纤程不能调用 Windows API,因为所有纤程共享线程环境块(TEB)。这会产生一个问题,当 Windows API 函数将状态信息放到一个纤程 TEB 上,另一个纤程会改写这个信息。UMS 对这个问题的解决方法是,为每个用户模式线程提供它自己的上下文。
此外,与纤程不同的是,UMS 一般不是直接为程序员使用的。编写用户模式调度程序的具体细节可能很有挑战,而且 UMS 并不包括这种调度程序。不过,调度程序来自 UMS 之上的编程语言库。
例如,微软提供并发运行时库(ConcRT),这是一个 C++ 并发编程框架,用于在多核处理器上设计并行任务(更深入了解请猛击:《什么是多核》一文)。ConcRT 提供用户模式调度程序,并能将程序分解成任务,以便在可用处理核上进行调度。