+
Skip to content

并发系统:调度器设计 #8

@htoooth

Description

@htoooth

通用调度器实现、应用场景及 Go/Erlang 特化分析

1. 计算中的调度导论

1.1 调度器及其核心目的的定义

在计算系统中,调度器是负责管理多个任务(如进程、线程、协程等)在有限处理资源(主要是 CPU)上执行的基础组件 1。其核心问题在于如何在竞争任务之间分配有限的 CPU 时间,以达成特定的系统目标 3。可以将调度器想象成繁忙交叉口的交通指挥员,决定哪些车辆(任务)可以通行,以确保交通顺畅,防止任何一条车道(任务)独占道路(CPU)3。其根本目标是保持计算资源(如 CPU 核心)的繁忙状态(负载均衡),并允许多个用户或任务有效地共享这些资源 4。

1.2 关键职责

调度器的核心职责涵盖了任务生命周期的管理和资源的分配决策:

  • 任务管理 (Task Management): 调度器需要跟踪任务的不同状态(例如,新建、就绪、运行、等待/阻塞、完成),并管理这些状态之间的转换。这通常涉及到维护各种队列,如就绪队列(存放准备运行的任务)和等待队列(存放等待 I/O 或其他事件的任务)1。
  • 资源分配 (Resource Allocation): 调度器的核心决策功能是选择下一个应该获得 CPU 执行权的就绪任务 1。这本质上是在匹配资源的“供给”(可用的 CPU 核心和时间片)与工作负载的“需求”(任务执行所需的计算资源)6。调度器会持续跟踪资源分配情况,以确保工作负载不会超出可用资源的限制 6。
  • 执行顺序确定 (Execution Ordering): 基于预定义的调度算法和策略,调度器决定任务的执行顺序 2。

1.3 基本概念

理解调度器需要掌握几个基本概念:

  • 抢占式 (Preemptive) vs. 非抢占式 (Non-Preemptive) 调度:
    • 非抢占式: 一旦任务开始运行,它将持续占用 CPU 直到完成、自愿阻塞(例如,等待 I/O)或主动让出 CPU 1。这种方式实现简单,开销较低,但如果一个长任务长时间占用 CPU,会导致其他任务响应缓慢(称为“护航效应”)3。
    • 抢占式: 调度器可以强制中断当前正在运行的任务(通常通过定时器中断或更高优先级任务到达的信号),并将 CPU 分配给另一个任务 1。这种方式提高了系统的响应性和公平性,但引入了额外的复杂性和开销(如上下文切换)8。抢占式调度对于分时系统和实时系统至关重要 1。
  • 上下文切换 (Context Switching): 这是操作系统或运行时在切换 CPU 执行权时,保存当前运行任务的状态(上下文,例如程序计数器、寄存器值、内存管理信息等,通常存储在进程控制块 PCB 或线程控制块 TCB 中)并加载下一个要运行任务的状态的过程 1。上下文切换使得多任务处理成为可能,但它本身会消耗 CPU 时间,带来一定的开销(称为分派延迟)1。执行实际切换操作的模块称为分派器 (Dispatcher) 1。

任何调度器,无论其应用领域(操作系统、语言运行时、数据库等),其核心功能都是通过在任务需求和资源可用性之间进行协调,来管理有限处理资源的争用。这种协调由特定的系统目标(如公平性、吞吐量、延迟)指导 1。抢占式和非抢占式调度之间的区别代表了系统响应性/公平性与实现复杂性/开销之间的基本权衡。抢占对于交互式和实时系统是必需的,但相比非抢占式调度,它需要更复杂的机制(如定时器中断、上下文切换)1。

2. 操作系统调度器:基础

操作系统 (OS) 调度器是管理计算机硬件资源(尤其是 CPU)与运行中进程和线程之间交互的基础。它们通常分为几个不同的层次,处理不同时间尺度上的调度决策。

2.1 操作系统调度器的类型

根据其功能和执行频率,OS 调度器通常分为三类:

  • 长期调度器 (Long-Term Scheduler) 或作业调度器 (Job Scheduler): 负责从后备作业池(通常在磁盘上)中选择进程,并将它们加载到主内存中进入就绪队列 1。它的主要作用是控制系统的多道程序度 (Degree of Multiprogramming),即同时存在于内存中准备运行的进程数量 1。长期调度器通常试图平衡 CPU 密集型(计算量大)和 I/O 密集型(等待 I/O 操作时间长)的进程,以提高整体系统效率 1。它运行的频率最低 3。在许多现代分时操作系统(如 Windows、Linux 的桌面环境)中,可能没有明确的长期调度器,新进程会直接进入内存由短期调度器管理 1。批处理系统中的作业调度是其典型应用 2。
  • 中期调度器 (Medium-Term Scheduler): 负责在主内存和二级存储(如硬盘)之间进行进程交换 (Swapping) 1。当内存不足或需要改善进程组合(例如,减少过多阻塞的进程)时,中期调度器可以将某些进程(通常是阻塞或低优先级的)暂时移出内存(换出),以释放空间给其他进程。之后,这些被换出的进程可以被重新调入内存(换入)并从中断处继续执行 2。它通过调整内存中的进程数量来间接控制多道程序度,并与虚拟内存管理紧密相关 2。其运行频率介于长期和短期调度器之间 3。例如,Linux 和 Windows 中的换页和交换机制就体现了中期调度的功能 2。
  • 短期调度器 (Short-Term Scheduler) 或 CPU 调度器 (CPU Scheduler): 这是最活跃的调度器,负责从就绪队列中选择下一个要分配 CPU 的进程或线程 1。它执行得非常频繁(可能每隔几毫秒),直接影响系统的响应速度和性能 3。短期调度器做出决策后,会调用分派器 (Dispatcher) 来执行实际的上下文切换,将 CPU 控制权交给选定的进程 1。其主要目标是优化 CPU 利用率和最小化任务响应时间 1。它是三种调度器中速度最快的 1。Linux 的 CFS 和 Windows 的优先级调度器都是短期调度器的例子 2。

2.2 常见调度算法

短期调度器使用各种算法来决定哪个就绪任务获得 CPU。以下是一些经典的算法:

  • 先来先服务 (First-Come, First-Served, FCFS): 最简单的非抢占式算法。进程按照它们到达就绪队列的顺序执行 3。易于实现,通常使用 FIFO 队列管理 7。主要缺点是可能导致“护航效应 (Convoy Effect)”,即一个耗时长的进程阻塞了后面所有(即使是耗时短的)进程,导致平均等待时间很差 3。
  • 最短作业优先 (Shortest Job Next, SJN / Shortest Job First, SJF): 选择预计执行时间最短的进程运行。可以是非抢占式的(一旦开始就运行到结束或阻塞),也可以是抢占式的(称为最短剩余时间优先, Shortest Remaining Time First, SRTF),即当一个新进程到达且其预计执行时间比当前运行进程的剩余时间还短时,会发生抢占 3。SJF 在理论上可以最小化平均等待时间 3。但它存在两个主要问题:一是难以准确预测未来的执行时间 13,二是可能导致长作业饥饿 (Starvation),即长时间得不到 CPU 3。
  • 轮转法 (Round Robin, RR): 一种常用的抢占式算法,特别适用于分时系统 3。每个进程被分配一个固定的、较小的时间片 (Time Slice / Quantum) 3。进程按循环队列的顺序运行,如果在时间片结束时进程仍在运行,则被抢占并移到队列末尾 5。RR 提供了较好的公平性和响应时间,特别是对于交互式任务 3。其性能对时间片大小敏感:时间片太大则趋近于 FCFS,太小则上下文切换过于频繁,导致系统开销增大 3。
  • 优先级调度 (Priority Scheduling): 为每个进程分配一个优先级,调度器总是选择就绪队列中优先级最高的进程运行 3。优先级可以是静态赋予的,也可以是动态调整的 10。它可以是抢占式的(高优先级进程到达时可以抢占低优先级进程)或非抢占式的 1。这种方法能确保关键任务优先执行 3。主要缺点是可能导致低优先级进程饥饿 3。可以通过老化 (Aging) 技术来缓解饥饿,即逐渐提高等待时间过长进程的优先级 5。
  • 多级队列 (Multilevel Queue, MLQ): 将就绪队列划分为多个独立的队列,例如,可以按进程类型(如前台交互式进程、后台批处理进程)或优先级分组 3。每个队列可以拥有自己的调度算法(例如,交互式队列使用 RR,批处理队列使用 FCFS)3。队列之间通常采用固定优先级的抢占式调度,即只有在高优先级队列都为空时,低优先级队列中的进程才能运行 15。进程通常被永久分配到一个队列 15。这种方法的优点是能够为不同类型的进程提供差异化服务,缺点是缺乏灵活性,且如果高优先级队列持续繁忙,低优先级队列可能饥饿 15。
  • 多级反馈队列 (Multilevel Feedback Queue, MLFQ): 是 MLQ 的改进,允许进程在不同队列之间移动 13。其目标是在不知道进程执行时间的情况下,同时优化周转时间(像 SJF)和响应时间(像 RR)17。它利用反馈机制动态调整进程优先级:通常,新进程进入最高优先级队列(假设其运行时间短);如果进程用完了其在某个队列的时间片,它会被降级到更低优先级的队列(通常具有更长的时间片);如果进程在时间片用完前主动放弃 CPU(例如,进行 I/O 操作),它可能会被提升或保持在当前优先级 17。为了防止饥饿,可以采用老化机制,将长时间在低优先级队列中等待的进程提升到较高优先级队列 13。MLFQ 非常灵活,能适应不同类型的进程行为,但实现复杂,需要仔细调整参数(队列数量、各队列调度算法、时间片长度、升级降级策略)17。此外,还存在进程可能“欺骗”调度器以维持高优先级的问题 17。

2.3 案例研究:Linux 完全公平调度器 (CFS)

CFS 是 Linux 内核中针对普通(非实时)任务的默认调度器 20。它取代了早期内核中的 O(1) 调度器。

  • 核心目标: 实现 CPU 时间的完全公平 (Completely Fair) 分配。理想情况下,如果有 N 个可运行任务,每个任务应该获得 1/N 的 CPU 时间 20。CFS 试图在实践中逼近这种理想状态,根据任务的权重(由 nice 值导出)按比例分配 CPU 时间 20。
  • 机制: CFS 的核心是虚拟运行时 (virtual runtime, vruntime) 的概念 20。vruntime 记录了一个任务在“理想公平处理器”上本应运行的时间。它随着任务实际运行时间的增加而增加,但增加的速度与任务的优先级(权重)成反比,即高优先级任务的 vruntime 增长较慢。CFS 调度器总是选择当前就绪队列中 vruntime 最小的任务来运行 20。
  • 数据结构: CFS 在每个 CPU 上维护一个红黑树 (red-black tree) 来存储可运行的调度实体(通常是任务)20。树中的节点按照 vruntime 排序,vruntime 最小的节点位于树的最左侧 22。选择下一个要运行的任务(即查找最左节点)的操作是 O(1) 的(因为最左节点通常被缓存),而插入和删除任务的操作复杂度是 O(logN),其中 N 是树中的任务数量 22。
  • 时间片: CFS 不使用固定的时间片。任务的运行时间取决于调度周期 (scheduling period / latency)、当前可运行任务的数量以及它们的权重 23。高优先级的任务因为其 vruntime 增长较慢,自然会获得更长的总运行时间。
  • 公平性增强: CFS 包含一些特性来改善交互式应用的性能,例如自动分组 (auto-grouping),它倾向于将父进程和子进程视为一个组进行调度,以提高桌面应用的响应速度 22。

从 FCFS、SJN 等简单算法到 MLFQ、CFS 等复杂算法的演进,反映了通用操作系统在平衡相互竞争的调度目标(如吞吐量、响应时间、公平性)以及适应多样化工作负载(交互式、批处理、CPU 密集型、I/O 密集型)方面的持续努力 3。这些更复杂的算法试图动态地适应任务行为,以克服简单静态策略的局限性。

此外,调度中的“公平性”概念并非绝对,而是依赖于具体的策略。例如,RR 追求时间片的均等分配 3,而 CFS 则追求基于优先级的按比例时间分配 20,MLFQ 则通过动态调整优先级来实现响应性和吞吐量的平衡,这是一种基于行为的公平性 17。

操作系统调度器通常在比语言运行时调度器(如 Go 或 Erlang 的调度器)更粗的粒度上运行(管理进程或内核线程)1。并且,OS 调度器的决策会受到底层硬件特性的直接影响,如多核、CPU 缓存、NUMA 架构等 9,而用户级调度器可能需要间接适应或抽象这些硬件细节。

3. 并发运行时调度器:超越操作系统

随着并发编程模型的发展,许多现代编程语言和运行时环境引入了自己的调度器,在操作系统调度器之上提供更细粒度的任务管理。

3.1 用户级线程 vs. 内核级线程

理解运行时调度器的基础在于区分两种主要的线程实现方式:

  • 内核级线程 (Kernel-Level Threads, KLTs): 由操作系统内核直接支持和管理 25。每个线程都有一个内核维护的线程控制块 (TCB)。内核负责 KLT 的调度。一个线程的阻塞系统调用通常只阻塞该线程本身,不影响同一进程中的其他 KLT 25。Linux 的 pthreads 和 Windows 线程是典型的 KLT 25。KLT 的创建、销毁和上下文切换开销相对较大,因为需要涉及内核模式的转换 27。
  • 用户级线程 (User-Level Threads, ULTs): 完全在用户空间由运行时库或虚拟机管理,内核对其存在并不知晓(或知之甚少)25。ULT 的创建、销毁和上下文切换非常快速,因为它们不涉及内核调用 25。然而,如果一个 ULT 执行了一个阻塞式的系统调用,那么承载它的 KLT 就会被阻塞,进而导致映射到该 KLT 上的所有其他 ULT 也无法运行(在 M:1 模型中)25。Go 的 Goroutine 和 Erlang 的进程就是 ULT 的例子 29。

3.2 M:N 调度模型

为了结合 ULT 的轻量级优势和 KLT 的真并行能力,M:N 模型应运而生。该模型将 M 个用户级线程映射到 N 个内核级线程上执行 29。通常情况下,M 远大于 N (M≫N),而 N 的数量通常与系统的 CPU 核心数相关 33。

  • 目标: 允许应用程序创建大量(可能是数百万个)轻量级的并发单元 (ULTs),而只使用少量(例如,每个 CPU 核心一个)相对较重的 KLTs 来实际执行它们,从而在实现高并发的同时控制系统开销 27。
  • 运行时调度器: M:N 模型的核心在于用户空间的运行时调度器。这个调度器负责管理 M 个 ULTs,并将它们动态地分配给可用的 N 个 KLTs 执行 29。操作系统内核仍然负责调度这 N 个 KLTs 到物理 CPU 核心上。
  • 阻塞处理: M:N 模型面临的一个关键挑战是如何处理阻塞的系统调用。当一个 ULT 发起阻塞调用时,其所在的 KLT 会被 OS 阻塞。如果运行时调度器不进行干预,那么该 KLT 上的其他就绪 ULTs 也将无法运行。因此,运行时调度器需要能够检测到这种情况,并将该 KLT 从其关联的逻辑处理器(或称为 P 概念,见 Go 调度器部分)上解绑,同时可能唤醒或创建一个新的 KLT 来接管该逻辑处理器,以继续执行其他就绪的 ULTs 25。

3.3 工作窃取 (Work-Stealing)

工作窃取是 M:N 调度器和任务并行系统中常用的一种高效、可扩展的负载均衡策略 33。

  • 原理: 在工作窃取模型中,每个工作单元(例如,执行 ULTs 的 KLT 或逻辑处理器 P)通常维护一个自己的本地任务队列 (local queue),通常是双端队列 (deque) 33。工作单元优先从自己的本地队列获取任务执行。当一个工作单元的本地队列变空时,它就变成一个窃取者 (thief),并随机选择另一个工作单元作为受害者 (victim),尝试从受害者的任务队列中“窃取”一个或多个任务来执行 33。
  • 优势:
    • 动态负载均衡: 能有效地将工作负载从未完成任务的工作单元转移到空闲的工作单元,保持所有处理资源(KLTs/核心)的繁忙状态,提高 CPU 利用率,尤其是在任务粒度较细或任务执行时间不均的情况下 33。
    • 可扩展性: 避免了使用单一全局任务队列带来的锁竞争和瓶颈问题,因为大部分时间工作单元都在访问自己的本地队列 43。
    • 数据局部性: 工作单元倾向于执行自己本地队列中的任务,这可能有助于提高缓存命中率(尽管窃取操作可能会破坏局部性)。
    • 迁移频率低: 与工作共享(work-sharing,即任务产生者主动推送任务给其他工作单元)相比,工作窃取只在工作单元空闲时才发生任务迁移,频率通常较低 33。
  • 开销: 工作窃取并非没有代价。窃取操作本身会引入动态开销 (dynamic overheads),这些开销随着并行度和窃取频率的增加而增长 38。这些开销包括:
    • 寻找受害者。
    • 访问(可能需要同步)受害者的队列 39。一些设计采用私有队列加消息传递的方式来避免直接并发访问 39。
    • 准备被窃取任务的执行上下文,这可能涉及检查受害者的执行状态(如堆栈)38。 因此,高效的工作窃取实现对于运行时性能至关重要 38。

3.4 用户级调度器中的抢占

与由 OS 负责抢占的 KLT 不同,ULTs 的抢占需要由用户级运行时调度器来实现(如果需要的话)30。

  • 协作式调度 (Cooperative Scheduling): 这是最简单的方式。ULTs 一直运行,直到它们自愿放弃控制权,例如通过显式调用 yield()、执行阻塞的通道操作、等待互斥锁或发起(可能阻塞的)系统调用 30。这种方式简单高效,但如果某个 ULT 陷入长时间计算而不主动让步,就会导致公平性问题,饿死同一 KLT 上的其他 ULTs 30。
  • 抢占式调度 (用户级实现): 为了防止长时间运行的 ULT 独占 KLT/核心,并保证公平性和响应性,用户级调度器可以实现抢占机制 30。常见技术包括:
    • 基于信号的抢占 (Signal-Based Preemption): 利用操作系统的定时器信号(如 POSIX 的 SIGALRM 或 SIGVTALRM)来中断正在执行 ULT 的 KLT 26。信号处理程序在用户空间运行,它可以通知用户级调度器保存当前 ULT 的上下文,并切换到另一个就绪的 ULT 26。Go 1.14 及以后版本采用了这种方式 43。这种方法需要精心的信号处理(例如,使用备用信号栈 sigaltstack)26。
    • 编译器插桩 (Compiler Instrumentation): 在编译时,在代码的特定位置(例如,函数入口、循环回边)插入检查指令 43。这些指令会检查一个全局的抢占标记。如果标记被设置(例如,由一个监控线程设置),该指令就会触发一次协作式的让步,将控制权交还给调度器。Go 1.2 到 1.13 版本使用了这种基于协作的抢占方式 43。这种方法的抢占点不如基于信号的精确。
    • 缩减计数 (Reduction Counting): 为每个 ULT 分配一个执行“预算”,这个预算通常以某种抽象的操作单位(如函数调用次数或执行的指令数)来衡量 11。当 ULT 的预算耗尽时,调度器就抢占它。Erlang/BEAM 使用这种方法 11。这种方法提供了确定性的公平性保证。

采用 M:N 模型是运行时环境做出的一种架构选择,目的是在应用程序层面获得对并发调度的细粒度控制,从而绕过 OS 调度器在管理应用级任务时可能存在的粗粒度和不可预测性。这种控制是以在用户空间重新实现复杂的调度逻辑(包括阻塞处理和抢占)为代价的 9。

对于可扩展的 M:N 调度器而言,工作窃取机制几乎是必需的。它解决了因使用多个本地队列(为避免全局锁竞争而必需)所带来的固有负载均衡问题,并且无需昂贵的集中式协调 33。

在用户层面实现抢占比依赖 OS 抢占要复杂得多。它需要编译器的配合(插桩)、复杂的信号处理机制或新颖的方法(如缩减计数),每种方法在精度、开销和实现难度上都有其权衡 11。这表明为 ULTs 实现类似 OS 的抢占是一个重要的工程挑战。

4. Go 运行时调度器:GMP 模型

Go 语言内置的运行时包含一个复杂的调度器,旨在高效地管理其核心并发原语——goroutine。该调度器采用了 M:N 模型,通常被称为 GMP 模型 29。

4.1 概述

Go 调度器的目标是在少量操作系统线程上调度可能存在的数百万个 goroutine,以实现高并发、低延迟和高 CPU 利用率 29。它完全在用户空间运行(由 Go 运行时管理),但会与内核交互以管理底层的 OS 线程 29。

4.2 GMP 组件

GMP 模型包含三个核心实体:

  • G (Goroutine): 代表一个 Go 函数的并发执行单元,通过 go 关键字启动 29。Goroutine 非常轻量级,其初始栈大小仅为几 KB(例如 2KB),并能根据需要动态增长和收缩 28。它们由 Go 运行时管理,而非操作系统直接管理 29。
  • M (Machine / OS Thread): 代表一个标准的操作系统内核级线程,由内核调度 29。M 是实际执行 Go 代码(goroutine)的载体 29。当 goroutine 执行阻塞的系统调用时,对应的 M 会被阻塞 29。运行时的 M 的数量可能超过 P 的数量,特别是当许多 M 因系统调用而阻塞时 29。Go 运行时会管理一个 M 的池。
  • P (Processor / Context): 代表一个逻辑处理器,是 G 执行所需的上下文环境 29。一个 M 必须获取 (acquire) 一个 P 才能执行 G 43。P 的数量由环境变量 GOMAXPROCS 或 runtime.GOMAXPROCS() 函数设置,默认值通常等于系统的 CPU 核心数 29。每个 P 都拥有一个本地运行队列 (Local Run Queue, LRQ),用于存放分配给它的可运行 G 29。

4.3 M:N 映射与 Goroutine 生命周期

Go 调度器负责将可运行的 G 动态地映射到与 P 关联的 M 上执行 33。

  • 创建与入队: 新创建的 G 通常会被放入当前 P 的 LRQ 29。如果 LRQ 已满,G 可能会被放入全局运行队列 (Global Run Queue, GRQ) 29。
  • 执行与调度: 与 P 关联的 M 会优先从 P 的 LRQ 中获取 G 来执行。如果 LRQ 为空,M 会尝试从 GRQ 获取(但检查频率较低,例如每 61 次调度检查一次,以避免 GRQ 饥饿和 LRQ 优先级过高的问题)或从其他 P 的 LRQ 中窃取 G 33。
  • 阻塞与让步: G 会一直运行,直到它发生阻塞(如等待 channel 操作、系统调用、互斥锁、或等待 GC)、主动让步(调用 runtime.Gosched())、执行完成、或被运行时抢占 34。

4.4 Go 中的工作窃取

工作窃取是 GMP 模型实现负载均衡的关键机制,自 Go 1.1 版本引入 33。

  • 触发条件: 当一个 M(绑定到 P)发现其 P 的 LRQ 为空时,它会启动工作窃取流程 33。
  • 窃取顺序:
    1. 检查自身的 P 的 LRQ。
    2. 检查 GRQ(按一定概率,如 1/61)33。
    3. 检查网络轮询器 (network poller) 是否有就绪的 G(等待网络 I/O 完成的 G)。
    4. 如果上述都没有找到 G,则随机选择另一个 P 作为受害者。
    5. 尝试从受害 P 的 LRQ 队尾窃取大约一半的 G 到自己的 LRQ 33。
  • 效果: 有效地在 P 之间重新分配 G,防止某些 P 空闲而另一些 P 过载,从而提高整体 CPU 利用率 33。

4.5 抢占机制

为了保证公平性,防止某个 G 长时间占用 M 而不让出,Go 的调度器实现了抢占机制,并且该机制经历了一个演化过程:

  • 协作式抢占 (Go 1.2 - 1.13): 依赖于编译器在函数调用(特别是函数入口或栈增长检查时)插入的检查点 43。如果 G 运行时间过长,运行时会设置一个抢占请求标志。当 G 执行到这些检查点时,会检查该标志并主动让出 M,触发调度。这种方式的缺点是,如果 G 执行一个没有函数调用的密集计算循环,它可能永远不会被抢占,导致同一 P 上的其他 G 饿死 43。
  • 异步(基于信号)抢占 (Go 1.14+): 为了解决协作式抢占的不足,Go 1.14 引入了基于信号的异步抢占 30。运行时有一个系统监控线程 (sysmon),它会定期检查所有 P。如果发现某个 G 在一个 M 上运行超过了一个阈值(例如 10 毫秒),sysmon 会向该 M 发送一个信号(例如 SIGURG)。M 接收到信号后,会中断当前 G 的执行,将其标记为可抢占,并在安全的时机(如函数调用返回时)将其放回队列,然后调度执行其他的 G 43。垃圾回收的栈扫描操作也可以触发抢占 43。这种方式提供了更可靠的抢占保证。

4.6 阻塞系统调用的处理

这是 M:N 调度器设计的核心难点之一。Go 运行时的处理方式如下:

  • 检测与解绑: 当一个 G 即将执行可能阻塞的系统调用时,运行时会介入 29。
  • M 与 P 分离: 正在执行该 G 的 M 会与它当前绑定的 P 解绑。这个 M 将进入阻塞状态,等待系统调用返回 29。
  • P 的持续运行: 运行时会尝试寻找一个空闲的 M(可能来自 M 的缓存池,或者创建一个新的 M)来接管这个 P 29。这样,P 就可以继续从其 LRQ 中调度并执行其他的 G,避免了因单个系统调用阻塞而导致整个逻辑处理器停顿。
  • “自旋”线程: 为了减少 M 阻塞/唤醒的开销,空闲的 M 在进入睡眠前可能会“自旋”一小段时间,主动寻找可运行的 G 或可用的 P 33。
  • 系统调用返回: 当系统调用完成时,原来的 G 变为可运行状态。执行该调用的 M 会尝试获取一个空闲的 P。如果成功,M 就绑定到 P 上并继续执行 G。如果找不到空闲的 P,这个 G 会被放入 GRQ,而该 M 可能会被置于空闲状态(放入 M 缓存池)等待后续任务 29。

4.7 为 Go 的需求定制通用调度器

要从一个通用的调度器框架演变成类似 Go 的 GMP 模型,需要进行以下特化:

  1. 实现 G、M、P 实体: 定义轻量级的 G 结构(包含小而可扩展的栈)、OS 线程 M,以及逻辑处理器 P(包含 LRQ 等状态)28。
  2. 构建 M:N 映射: 实现 M 绑定 P、G 在 P 的 LRQ 和 GRQ 之间流转的逻辑 43。
  3. 实现工作窃取: 实现 Go 特定的窃取逻辑:空闲 M/P 检查 LRQ -> GRQ -> 网络轮询器 -> 随机窃取其他 P 的 LRQ 33。
  4. 实现异步抢占: 使用定时器和 OS 信号机制,实现对运行时间超过阈值(如 10ms)的 G 的抢占 35。
  5. 处理阻塞系统调用: 实现系统调用入口/出口的钩子,用于 M 与 P 的解绑/重新绑定,以及 M 的创建/休眠/唤醒管理 29。
  6. 与 Go 特性集成: 使 channel 操作、select 语句、runtime.Gosched() 成为调度点;集成网络轮询器以高效处理网络 I/O;与并发垃圾回收器协调(GC 暂停、GC 触发抢占等)28。
  7. 调优参数: 提供 GOMAXPROCS 这样的参数来控制 P 的数量 35。

GMP 模型不仅仅是一个通用的 M:N 实现,而是针对 Go 语言特性(goroutine 的轻量级、channel 通信)和其主要应用场景(高并发网络服务)进行了深度优化的结果。P 这个抽象层级、具体的工作窃取策略(结合了 LRQ, GRQ, 网络轮询器)、以及抢占机制的演变都体现了为 Go 的特定需求所做的设计决策 33。

Go 抢占机制从协作式演进到基于信号的异步抢占,清晰地展示了其务实的设计哲学:为了解决早期版本中遇到的实际性能和公平性问题(例如,CPU 密集型循环饿死其他 goroutine),不惜增加实现的复杂性来换取更健壮的实际运行表现 43。

GOMAXPROCS 参数的重要性不言而喻,它直接控制了可用于并发执行 Go 代码的逻辑处理器 P 的数量,从而决定了程序可以利用的并行度。合理设置该值(通常与物理核心数匹配,但在容器化环境中可能需要调整 54)对于平衡并行带来的好处与潜在的开销(如 M 之间的上下文切换、缓存争用)至关重要 29。

5. Erlang 运行时调度器:BEAM 模型

Erlang/OTP 平台的虚拟机 BEAM (Bogdan's Erlang Abstract Machine) 包含一个为大规模并发、高容错性和软实时系统设计的调度器。其设计深受Actor 模型的影响 32。

5.1 基于进程的并发 (轻量级进程)

Erlang 并发的基础是Erlang 进程 (Erlang process),这是一种极其轻量级的执行单元,与操作系统的进程或线程有很大区别 32。

  • 隔离性: 每个 Erlang 进程都拥有自己独立的堆内存空间和邮箱 (mailbox),进程之间不共享内存 51。这种彻底的隔离是 Erlang 容错模型的基础。
  • 轻量级: Erlang 进程的创建和销毁开销极小,使得系统可以轻松创建和管理数百万个并发进程 32。
  • 通信: 进程间唯一的通信方式是异步消息传递 (asynchronous message passing) 32。发送进程将消息发送到接收进程的邮箱,发送操作是非阻塞的。消息在传递时通常会被复制,以保证进程间的内存隔离 53。

5.2 通过缩减计数的抢占式调度

BEAM 采用的是抢占式调度 (preemptive scheduling) 策略,以保证公平性和系统的响应性,这对于其软实时目标至关重要 11。

  • 抢占单位:缩减 (Reductions): BEAM 的抢占并非基于时间片,而是基于缩减计数 11。一次“缩减”大致对应于一次函数调用或其他一些基本操作。运行时会为每个 Erlang 进程维护一个缩减计数器。
  • 抢占机制: 每个进程在被调度执行时,会被分配一个缩减预算(例如,早期版本中是 2000 次缩减,这个值可以调整)11。当一个进程执行的操作耗尽了它的缩减预算时,调度器就会抢占该进程,即使它尚未完成当前的工作,然后选择另一个就绪的进程来执行。这种机制确保了没有哪个进程能够长时间霸占调度器线程,从而保证了所有进程都能获得公平的执行机会 11。

5.3 SMP 支持与调度器均衡

现代 BEAM 虚拟机支持对称多处理 (Symmetric Multiprocessing, SMP),能够利用多核处理器的优势 11。

  • 调度器线程: BEAM 通常会启动多个调度器线程 (scheduler threads),数量一般等于 CPU 核心数(可配置)11。每个调度器线程运行在一个独立的 OS 线程上。
  • 运行队列的演进:
    • 早期 SMP 支持(如 R11B/R12B 版本)使用一个共享的全局运行队列 (run queue) 来存放所有可运行的 Erlang 进程 11。然而,随着调度器线程数量的增加,对这个共享队列的访问成为了性能瓶颈,因为需要锁来保护 11。
    • 后续版本(如 R13B 及以后)改进为每个调度器线程拥有自己的运行队列 11。这消除了全局队列的锁竞争问题。
  • 负载均衡 (迁移逻辑): 为了解决每个调度器拥有独立队列可能导致的负载不均衡问题(即某些调度器忙碌而另一些空闲),BEAM 引入了迁移逻辑 (Migration Logic) 11。这本质上是一种工作窃取或工作共享机制,允许调度器线程在彼此的运行队列之间迁移 Erlang 进程,以平衡负载,确保公平性和资源利用率 11。
  • 调度器配置: 可以配置最大可用调度器线程数和当前在线(活动)的调度器线程数 11。

5.4 Actor 模型的影响

Erlang 的并发模型和调度器设计是 Actor 模型思想的直接体现 57。

  • Actor 即进程: Erlang 进程扮演了 Actor 模型中 Actor 的角色:它们是独立的计算单元,拥有私有状态(内存)和行为 57。
  • 消息驱动: Actor 之间的交互完全通过异步消息传递进行,消息被放入接收 Actor 的邮箱(mailbox)57。
  • 响应行为: Actor 收到消息后,可以执行计算、改变自身状态、向其他 Actor 发送消息或创建新的 Actor 58。
  • 并发、分布与容错: Actor 模型的这些特性天然地支持了大规模并发、分布式系统构建以及 Erlang 著名的容错能力(通过监督树和“let it crash”哲学实现)51。

5.5 优先级级别

Erlang 进程可以被赋予不同的优先级(例如,low, normal, high, max)11。

  • 调度规则: 调度器在选择下一个要运行的进程时会考虑优先级。通常,在同一优先级内部采用轮转(基于缩减计数)的方式调度 11。
  • 优先级抢占: 优先级高的进程会抢占优先级低的进程。max 优先级最高,其次是 high,然后是 normal 和 low 12。max 和 high 优先级是严格的,即只有当更高优先级的队列为空时,才会执行较低优先级的进程 12。normal 和 low 之间的调度可能更复杂,例如,调度器可能在执行了一定数量的 normal 进程的缩减后,再执行一个 low 进程 12。
  • 使用建议: 尽管存在优先级系统,但 Erlang/OTP 官方和社区通常不鼓励开发者随意使用 high 或 low 优先级,因为这很容易导致进程饥饿或优先级反转等问题 12。max 优先级仅供系统内部使用 12。默认的 normal 优先级配合缩减计数抢占通常能提供足够的公平性和响应性。

5.6 为 Erlang 的需求定制通用调度器

要构建一个类似 BEAM 的调度器,需要关注以下 Erlang 特有的需求:

  1. 实现 Actor/轻量级进程: 核心是实现具有独立内存(堆)、独立邮箱、并且极其轻量级的进程抽象 32。
  2. 实现基于缩减计数的抢占: 这是 BEAM 调度器的标志性特征。需要精确地跟踪每个进程执行的操作(如函数调用)并累加缩减计数,在达到预算时强制进行上下文切换 11。这与基于时间片或信号的抢占有本质区别。
  3. 支持 SMP 和负载均衡: 实现多调度器线程架构,每个调度器管理自己的运行队列 11。同时,必须实现有效的迁移逻辑(工作窃取或共享)来在调度器之间平衡进程负载 11。
  4. 实现优先级队列: 至少支持 Erlang 的四个优先级级别,并实现它们之间的抢占规则(高优先级严格抢占低优先级)11。
  5. 集成消息传递和 GC: 调度器需要与异步消息传递机制紧密集成(例如,当消息到达邮箱时唤醒等待的进程)32。还需要与每个进程的垃圾回收 (per-process GC) 机制协同工作 53。

Erlang 的调度器与其基于 Actor 的并发模型、消息传递通信、独立的内存管理(包括 per-process GC)以及其对容错和软实时性的追求是深度耦合、相辅相成的。缩减计数抢占、进程隔离等并非孤立特性,而是支撑整个 Erlang 设计哲学的关键组成部分 11。

BEAM 在 SMP 支持上的演进(从共享队列到每调度器队列加迁移逻辑)与其他通用 OS 或运行时调度器(如 Go 的 GMP)所面临和解决的挑战(避免中心化瓶颈、实现分布式负载均衡)如出一辙,这揭示了可扩展调度器设计中的普遍规律 11。

尽管 Erlang 提供了进程优先级设置,但在实践中却不鼓励使用,这暗示其核心的缩减计数公平机制对于典型的 Erlang 应用场景通常被认为是足够健壮和有效的,手动调整优先级反而可能引入更多复杂性和潜在问题 12。

6. 对比分析:Go vs. Erlang vs. OS 调度器

操作系统调度器、Go 运行时调度器(GMP)和 Erlang 运行时调度器(BEAM)在设计目标、核心机制和权衡取舍上存在显著差异。

6.1 关键架构差异

  • 调度单元:
    • OS: 内核级线程 (KLTs) 25。
    • Go (GMP): Goroutines (用户级线程 ULTs),通过 M:N 模型映射到 KLTs 33。
    • Erlang (BEAM): 轻量级进程 (类似 Actor),通过 M:N 模型映射到 KLTs 32。
  • 内存模型:
    • OS: 同一进程内的线程共享内存空间 27。
    • Go: Goroutine 共享进程的内存空间。虽然推荐通过 channel 进行通信,但直接内存共享是可能的,也是需要注意并发安全的地方 27。
    • Erlang: 进程拥有独立的堆内存,进程间无共享内存。通信完全通过消息传递(通常涉及数据复制)完成 51。
  • 调度器位置:
    • OS: 内核空间 25。
    • Go: 主要在用户空间(运行时),但需要内核调度底层的 M (KLTs) 29。
    • Erlang: 主要在用户空间(BEAM 虚拟机),但需要内核调度底层的调度器线程 (KLTs) 32。

6.2 阻塞调用与 I/O 处理

  • OS: 阻塞的系统调用会阻塞执行该调用的 KLT。OS 调度器会切换到另一个就绪的 KLT。
  • Go: 阻塞的系统调用会阻塞执行它的 M。Go 运行时检测到这种情况,会将 M 与 P 解绑,并可能启动或唤醒另一个 M 来接管 P,以继续执行 P 上的其他 G。对于网络 I/O,Go 运行时使用高效的网络轮询器 (netpoller) 实现非阻塞操作,goroutine 在等待 I/O 时会被挂起,但 P 不会被阻塞 29。
  • Erlang: 阻塞操作(如等待消息 receive、执行某些 I/O 操作)会导致 Erlang 进程被挂起,调度器线程可以转而执行其他就绪的进程。BEAM 通常通过内置函数 (BIFs) 或端口 (ports) 来异步处理 I/O 12。

6.3 抢占策略

  • OS: 通常基于时间片抢占(由定时器中断触发)9。像 CFS 这样的算法旨在提供按比例的公平性 20。
  • Go: 从基于函数调用的协作式抢占演进为基于信号的异步抢占,主要基于时间阈值(例如 >10ms)来防止 G 长时间运行 35。目标是在用户级模拟 OS 级别的抢占公平性。
  • Erlang: 基于缩减计数 (reduction counting) 的抢占 11。根据进程完成的工作量(而非时间)来保证公平性,这是其软实时特性的基础。

6.4 内存管理 / GC 交互

  • OS: 负责进程的虚拟内存管理,不直接参与语言层面的垃圾回收 (GC)。
  • Go: 拥有一个并发的垃圾回收器,但仍然存在短暂的全局“停止世界 (Stop-The-World, STW)”阶段,尽管已大大缩短。GC 的活动会影响调度(例如,GC 暂停期间调度暂停,GC 辅助任务需要调度),GC 也可以触发抢占 34。共享内存模型使得 GC 比隔离堆模型更复杂 53。
  • Erlang: 采用每个进程独立的垃圾回收 (Per-process GC) 53。GC 只发生在单个进程的堆上,不会产生全局的 STW 暂停。这极大地提高了系统的响应性和容错性,是进程隔离内存模型带来的关键优势 53。

6.5 错误处理哲学

  • OS: 处理硬件异常和信号。通常情况下,一个进程的崩溃不会影响其他进程。
  • Go: 强调显式的错误处理(通过函数返回值)。panic(类似异常)通常被认为是不可恢复的,除非在 goroutine 内部被 recover 捕获,否则会导致整个程序终止,这主要是因为共享内存模型下难以保证其他 goroutine 的状态一致性 51。需要开发者仔细管理错误流。
  • Erlang: 奉行**“让它崩溃 (Let it Crash)”的哲学,并结合 OTP (Open Telecom Platform) 框架中的监督树 (Supervision Trees)** 51。由于进程是隔离的,一个进程的错误(崩溃)不会直接影响其他进程。监督进程负责监控其子进程,当子进程崩溃时,监督者可以根据预定义的策略(如重启进程)来恢复服务。容错是语言和平台设计的核心部分 51。

6.6 特性比较表

下表总结了三类调度器的关键特性差异:

特性 操作系统调度器 (通用) Go 调度器 (GMP) Erlang 调度器 (BEAM)
调度单元 内核级线程 (KLT) Goroutine (ULT, M:N 映射到 KLT) Erlang 进程 (Actor/ULT, M:N 映射到 KLT)
内存模型 线程共享进程内存 Goroutine 共享进程内存 (推荐 Channel 通信) 进程隔离堆 (无共享内存), 消息传递 (复制)
调度器位置 内核空间 用户空间 (运行时) + 内核 (调度 M) 用户空间 (VM) + 内核 (调度 Scheduler Thread)
阻塞系统调用处理 阻塞 KLT, OS 切换其他 KLT 阻塞 M, Runtime 解绑 M 与 P, 启动新 M 接管 P 阻塞 Erlang 进程, Scheduler Thread 执行其他进程
抢占机制 时间片 (定时器中断) / 优先级 异步信号抢占 (基于时间 > 10ms) 缩减计数抢占 (基于工作量)
垃圾回收 (GC) 不涉及 并发 GC (有短暂 STW), 全局堆 Per-process GC (无全局 STW), 独立堆
错误处理范式 进程隔离, 信号/异常 显式错误值返回, Panic (通常程序终止) Let it Crash + Supervisor (OTP), 进程隔离
典型应用场景 通用计算, 桌面/服务器 OS 高并发网络服务, 微服务 高并发, 高可用, 容错, 软实时系统 (电信, 分布式)

内存模型的选择(共享 vs. 隔离)是 Go 和 Erlang 并发方式最根本的区别之一,其影响深远,贯穿了调度器设计、GC 实现乃至错误处理哲学 27。Go 的共享内存模型带来了低成本数据共享的可能性,但也引入了数据竞争的风险、全局 GC 的挑战以及更脆弱的错误处理(一个 goroutine 的严重错误可能污染共享状态,导致整个程序需要终止)。相比之下,Erlang 的进程隔离模型强制使用消息传递(带来数据复制开销),但换来了无数据竞争、per-process GC(无全局暂停)以及强大的“let it crash”容错能力。

尽管 Go 和 Erlang 都实现了 M:N 用户级调度,但它们的抢占策略反映了不同的核心优先级。Go 的基于时间的抢占主要是为了解决由长时间运行循环引起的病态性能和公平性问题 35。而 Erlang 基于工作量(缩减计数)的抢占则旨在提供可预测的、细粒度的公平性,以支持其软实时行为 11。

7. 专业化调度场景

除了通用的操作系统调度器和语言运行时调度器,许多特定领域也发展出了高度专业化的调度机制。

7.1 实时操作系统 (RTOS)

RTOS 用于那些对任务完成时间有严格要求的系统 4。其核心目标是确定性 (Determinism)可预测性 (Predictability),确保关键任务能在其截止时间 (Deadline) 前完成 68。根据对截止时间要求的严格程度,分为:

  • 硬实时 (Hard Real-Time): 错过截止时间即为系统失败(例如,飞行控制系统、心脏起搏器)68。
  • 软实时 (Soft Real-Time): 允许偶尔错过截止时间,但系统性能会下降(例如,流媒体播放)68。

RTOS 通常采用基于优先级的抢占式调度 68。两种经典的 RTOS 调度算法是:

  • 速率单调调度 (Rate Monotonic Scheduling, RMS):
    • 一种静态优先级 (Static-Priority) 算法 70。
    • 优先级根据任务的周期 (Period) 分配:周期越短,优先级越高 68。
    • 在固定优先级算法中是最优的 70。
    • 可调度性分析 (Schedulability Analysis) 通常基于 CPU 利用率。对于一组周期性、独立、截止时间等于周期的任务,如果总 CPU 利用率不超过某个阈值(例如,Liu & Layland 证明的理论上限约为 ln(2)≈69.3% 对于任意数量任务),则 RMS 可以保证所有任务满足截止时间 73。
    • 实现相对简单,行为在过载时可预测(低优先级任务先错过截止时间)70。
    • 主要假设:任务是周期性的,独立的,且截止时间等于其周期 70。
  • 最早截止时间优先 (Earliest Deadline First, EDF):
    • 一种动态优先级 (Dynamic-Priority) 算法 68。
    • 优先级根据任务的绝对截止时间动态分配:截止时间越近,优先级越高 68。
    • 在单处理器抢占式系统中是最优的调度算法:如果存在任何调度算法能够调度一个任务集使得所有任务满足截止时间,那么 EDF 也能 73。
    • 理论上可以达到 100% 的 CPU 利用率,同时保证所有截止时间(只要总利用率不超过 100%)73。
    • 比 RMS 更灵活,可以处理周期性和非周期性任务 74。
    • 缺点:实现比 RMS 复杂 73;在系统过载时,哪些任务会错过截止时间变得不可预测 73;可能存在截止时间交换 (Deadline Interchange) 问题(类似于优先级反转)73。

7.2 数据库查询调度器

数据库管理系统 (DBMS) 内部通常包含复杂的调度机制,用于管理并发执行的查询和事务,以优化性能、保证公平性并维护数据一致性 (ACID 特性) 76。

  • 目标:
    • 高效分配资源(CPU、内存、I/O)给不同的查询 79。
    • 通过并发控制 (Concurrency Control) 机制(如锁、时间戳、多版本并发控制 MVCC)防止并发事务相互干扰,保证隔离性 (Isolation) 81。
    • 确保执行结果的正确性,通常要求达到可串行化 (Serializability),即并发执行的结果等同于某种串行执行的结果 78。这可以通过冲突可串行化或视图可串行化等技术实现 78。
    • 管理事务依赖关系,确保原子性 (Atomicity)持久性 (Durability),并支持故障恢复(例如,通过可恢复调度、无级联调度)78。
  • 实现特点:
    • 数据库调度器通常在 DBMS 内部实现,可能直接管理资源,绕过或补充 OS 调度器 79。
    • 现代数据库可能采用基于任务的并行 (Task-based Parallelism),将复杂查询分解为多个小任务,由数据库内部的任务调度器分派给工作线程执行 79。
    • 可以使用优先级策略(例如,基于查询类型、用户、资源消耗)进行调度,如步幅调度 (Stride Scheduling) 可以实现按比例的资源共享 79。
    • 还可能涉及对后台任务(如数据加载、备份、同步)的调度(例如,Oracle Scheduler 82, 阿里云 DTS 83)。
    • 查询优化(选择最高效的查询计划)与查询执行调度是相关但不同的两个阶段。

7.3 网络服务器请求调度器 (负载均衡器)

在分布式系统中,负载均衡器充当请求调度器的角色,将传入的客户端请求分发到后端的多台服务器上,以提高系统的吞吐量、可用性和响应速度 84。

  • 常用算法:
    • 轮询 (Round Robin, RR): 按顺序将请求依次发送给后端服务器 84。简单,适用于后端服务器性能相似的场景。不考虑服务器的当前负载或连接数 84。
    • 加权轮询 (Weighted Round Robin, WRR): 为每个服务器分配权重(通常基于其处理能力)。权重越高的服务器接收到的请求比例越大 84。适用于服务器性能不均的场景 84。仍然不动态考虑当前负载 84。
    • 最少连接 (Least Connections, LC): 将新请求发送给当前活动连接数最少的服务器 84。这是一种动态调度算法,能较好地处理请求负载变化的情况,适用于服务器性能相似的场景 84。TCP 连接的 TIME_WAIT 状态可能影响其在服务器性能差异较大时的均衡效果 84。
    • 加权最少连接 (Weighted Least Connections, WLC): 结合了权重和连接数。将请求发送给 (当前连接数 / 权重) 比值最低的服务器 85。适用于服务器性能不均且请求负载变化的场景 85。比 LC 计算量稍大,但能更有效地将请求分配给有能力处理它的服务器 85。
    • 一致性哈希 (Consistent Hashing): 根据请求的某个标识(如客户端 IP 地址、请求 URL)计算哈希值,并将请求发送到哈希环上对应的服务器 87。主要优点是当后端服务器增加或减少时,只有少量请求的映射会改变,这对于需要维持会话状态的应用(缓存、购物车等)非常有用 87。

这些专业化调度器的存在表明,通用的 OS 调度器虽然功能强大且用途广泛,但往往无法满足特定应用领域对性能保证、领域知识利用或细粒度控制的特殊需求 4。数据库需要考虑事务语义和 I/O 模式,负载均衡器关注请求分发和服务器状态,而 RTOS 则聚焦于严格的时间约束。

实时调度尤其突出了与通用调度不同的优化目标。通用调度器通常优化平均性能(如吞吐量、平均响应时间),而实时调度器则优先保证可预测性时间确定性,确保任务在截止时间前完成,即使这意味着牺牲一些平均性能或资源利用率 3。这导致了像 RMS 和 EDF 这样基于任务周期或截止时间进行调度的特定算法的产生。

8. 何时需要调度器?

调度器的必要性和复杂性取决于具体的计算场景。

8.1 需要复杂调度的场景

在以下情况下,复杂且精密的调度机制通常是必不可少的:

  • 多任务/多道程序环境: 当多个进程或线程需要并发执行并争夺有限的 CPU 资源时,调度器是核心组件,用于管理并发、提供公平性并保证系统响应 1。所有现代通用操作系统(如 Windows, Linux, macOS)都依赖复杂的调度器 2。
  • 高并发系统: 处理大量并发请求或任务的系统,如 Web 服务器、数据库服务器、大型应用服务器等,需要高效的调度来最大化吞吐量、最小化延迟并有效利用资源 6。Go 和 Erlang 等语言的运行时调度器就是为这类场景设计的 48。
  • 公平资源分配: 在多用户或多租户共享系统资源的环境中,调度器(如 Linux CFS)需要根据预设策略或优先级,公平地分配 CPU 时间 2。
  • 实时约束: 任何任务执行时间有严格截止时间要求的系统,都必须使用专门的实时调度器(如 RTOS 中的 RMS 或 EDF)来保证任务的及时完成 2。
  • 大量 I/O 密集型工作负载: 当系统中存在许多需要频繁等待 I/O 操作(如磁盘读写、网络通信)的任务时,调度器通过在任务阻塞时切换到其他可运行任务,能够显著提高 CPU 的利用率和系统整体吞吐量 2。
  • 图形用户界面 (GUI) 应用: GUI 应用需要快速响应用户输入(如鼠标点击、键盘输入)并流畅地更新界面。调度器需要优先处理 UI 相关的任务以保证良好的用户体验。

8.2 调度简化或无需调度的场景

在某些情况下,复杂的调度机制可能不是必需的,或者可以大大简化:

  • 简单的顺序执行程序: 仅包含单一执行流程、从头到尾依次执行指令的程序(例如,许多基本的命令行工具或脚本)不需要调度器,因为不存在并发执行或资源争用 2。
  • 单任务系统: 设计为一次只运行一个应用程序的操作系统或嵌入式系统(例如,早期的 DOS 或非常简单的专用设备)不需要复杂的调度 2。
  • 裸机嵌入式系统: 在没有操作系统的微控制器上运行的程序,如果只执行一个固定的循环或处理一组简单的、非并发的任务,可能完全不需要调度器,或者仅通过中断服务程序处理简单的任务切换 2。
  • 任务短暂且非阻塞的事件驱动应用: 在某些事件驱动模型中,如果事件处理函数(任务)执行时间非常短,并且从不执行阻塞操作,那么事件循环本身就扮演了一个非常基础的(通常是 FIFO 或基于优先级的)调度器角色。虽然仍然存在调度,但可能不需要像 OS 那样复杂的抢占式、多策略的 CPU 调度算法。然而,如果事件处理函数可能耗时较长,或者存在大量并发事件,那么仍然需要更复杂的调度策略来保证响应性。

8.3 调度器必要性场景对比表

下表总结了不同场景下对调度器复杂性的需求:

特征 需要复杂调度器 调度器简化/无需
并发程度 高 (多进程/线程/协程) 无或极低 (单任务/顺序执行)
资源争用 显著 (CPU, 内存等竞争激烈) 无或极低
性能目标 严格 (高吞吐量, 低延迟, 公平性, 满足截止时间) 宽松 (完成任务即可)
任务阻塞 常见 (大量 I/O, 同步等待) 无或极少 (任务短暂且非阻塞)
示例场景 现代 OS, 高并发服务器, 数据库, RTOS, GUI 应用 简单脚本, 单任务 OS, 裸机嵌入式, 极简事件循环

系统所需的调度器复杂性与并发度、资源争用程度以及性能需求的严格性直接相关。当并发任务多、资源竞争激烈、且对公平性、延迟或截止时间有高要求时,就需要复杂精密的调度机制 1。反之,在并发度低、资源充足、性能要求宽松的场景下,调度可以大大简化甚至省略 2。

即使在看似简单的系统中(如事件驱动系统),通常也存在某种形式的隐式调度(例如事件循环本身)。因此,问题往往不是“是否”需要调度,而是需要“多复杂”的调度机制 92。

9. 示例:一个简单的 JavaScript 任务调度器

JavaScript 在其主要的宿主环境(浏览器和 Node.js)中,通常基于单线程的事件循环 (Event Loop) 模型运行 92。这意味着在主线程上无法实现真正的并行执行(需要 Web Workers 等技术)。然而,JavaScript 运行时内部包含了调度机制来处理异步操作,我们可以利用其提供的原语来模拟一个简单的任务调度器。

9.1 JavaScript 异步原语与调度

JavaScript 提供了一些用于安排代码异步执行的函数,它们可以看作是与运行时内部调度器交互的接口:

  • setTimeout(callback, delay): 安排 callback 函数在至少 delay 毫秒之后执行 93。它将 callback 放入宏任务 (macrotask) 队列。实际执行时间会晚于 delay,因为它必须等待当前正在执行的同步代码完成,并且可能需要等待之前的宏任务完成 93。setTimeout(callback, 0) 常用于将任务推迟到下一个事件循环周期执行,实现一种协作式让步 (cooperative yielding),允许浏览器处理其他事件(如 UI 更新)94。
  • setInterval(callback, interval): 安排 callback 函数每隔 interval 毫秒重复执行一次 96。与 setTimeout 类似,它也使用宏任务队列,并且时间间隔不精确。
  • Promise.resolve().then(callback): 安排 callback 函数作为微任务 (microtask) 执行 92。微任务会在当前同步代码执行完毕后、下一个宏任务开始之前立即执行。微任务队列的优先级高于宏任务队列。这对于需要在当前事件循环周期内尽快执行的异步任务很有用。
  • requestAnimationFrame(callback): 主要用于浏览器环境中的动画更新 93。它安排 callback 函数在浏览器下一次重绘 (repaint) 之前执行 97。回调频率通常与显示器的刷新率同步(如 60Hz)97。相比 setTimeout,它能提供更平滑的动画效果,并且在页面不可见时会自动暂停以节省资源 93。它接收一个高精度的时间戳参数 97。
  • scheduler.postTask(callback, {priority}): 这是一个较新的 Web API,允许开发者以明确的优先级(如 'user-blocking', 'user-visible', 'background')来调度任务 94。它提供了比 setTimeout 更细粒度的控制,旨在帮助浏览器更好地管理任务,优化用户体验 98。

9.2 简单调度器模拟实现

我们可以使用 JavaScript 的异步原语来模拟一个基础的任务调度器,包含一个任务队列和调度逻辑。

概念:

  1. 维护一个任务队列(例如,一个数组)。
  2. 提供一个函数 addTask 将任务(通常是一个函数)添加到队列中。
  3. 实现一个调度循环函数 runNextTask。
  4. runNextTask 从队列中取出一个任务并执行。
  5. 为了避免长时间阻塞主线程,runNextTask 在执行完一个任务后,使用 setTimeout(runNextTask, 0) 或 Promise.resolve().then(runNextTask) 来异步地安排下一次调度循环的执行,从而让出控制权给事件循环。

示例代码:

JavaScript

class SimpleScheduler {
constructor() {
this.taskQueue =; // 任务队列 (简单 FIFO)
this.isSchedulerRunning = false; // 标记调度器是否正在运行
}

// 添加任务到队列
addTask(taskFn) {
if (typeof taskFn!== 'function') {
console.error("Task must be a function.");
return;
}
this.taskQueue.push(taskFn);
this.scheduleExecution(); // 尝试启动调度器
}

// 安排调度器执行 (如果尚未运行且队列中有任务)
scheduleExecution() {
if (!this.isSchedulerRunning && this.taskQueue.length > 0) {
this.isSchedulerRunning = true;
// 使用 setTimeout(..., 0) 将 runNextTask 放入宏任务队列
// 这会在当前同步代码执行完毕后执行,并允许事件循环处理其他事件
setTimeout(this.runNextTask.bind(this), 0);
}
}

// 执行队列中的下一个任务
runNextTask() {
if (this.taskQueue.length === 0) {
this.isSchedulerRunning = false; // 队列空,停止调度器
return;
}

// 从队列头部取出一个任务 (FIFO)  
const taskToRun \= this.taskQueue.shift();

try {  
  // 执行任务  
  console.log("Running task...");  
  taskToRun();  
  console.log("Task finished.");  
} catch (e) {  
  console.error("Task execution failed:", e);  
  // 可以添加错误处理逻辑,例如将任务重新排队或记录错误  
}

// 检查是否还有任务,并异步安排下一次执行  
if (this.taskQueue.length \> 0) {  
  // 再次使用 setTimeout(..., 0\) 安排下一次调度  
  // 这在每个任务执行后都提供了让步的机会  
  setTimeout(this.runNextTask.bind(this), 0);  
} else {  
  this.isSchedulerRunning \= false; // 队列空,停止调度器  
}  

}
}

// --- 使用示例 ---
const scheduler = new SimpleScheduler();

console.log("Adding tasks...");

scheduler.addTask(() => {
console.log("Task 1 executed.");
});

scheduler.addTask(() => {
console.log("Task 2 (simulating work) started...");
// 模拟一个耗时操作,但不阻塞太久,因为调度器会yield
const startTime = Date.now();
while (Date.now() - startTime < 20) {
// Busy wait for 20ms
}
console.log("Task 2 finished.");
});

scheduler.addTask(() => {
console.log("Task 3 executed.");
});

console.log("Tasks added to scheduler.");

// 可以继续添加更多任务
scheduler.addTask(() => {
console.log("Task 4 executed.");
});

// 输出顺序将大致是:
// Adding tasks...
// Tasks added to scheduler.
// Running task...
// Task 1 executed.
// Task finished.
// Running task...
// Task 2 (simulating work) started...
// Task 2 finished.
// Task finished.
// Running task...
// Task 3 executed.
// Task finished.
// Running task...
// Task 4 executed.
// Task finished.
// (注意:实际输出顺序和时间取决于事件循环的具体状态)

说明:

  • 这个 SimpleScheduler 类维护了一个 taskQueue 数组作为任务队列。
  • addTask 方法将函数推入队列,并调用 scheduleExecution。
  • scheduleExecution 检查调度器是否已在运行以及队列是否为空。如果需要启动,它使用 setTimeout(this.runNextTask.bind(this), 0) 将 runNextTask 的执行安排到事件循环的下一个宏任务周期。
  • runNextTask 是核心调度逻辑:它从队列中取出一个任务执行,然后在任务执行完毕后,如果队列中还有任务,它再次使用 setTimeout(..., 0) 来安排自身的下一次执行。这个 setTimeout 调用至关重要,它实现了任务间的协作式让步,防止单个长任务(或调度循环本身)独占主线程,允许浏览器响应用户交互、执行其他异步回调等。
  • 这个实现是一个简单的 FIFO 调度器。可以通过使用优先队列数据结构和相应的入队/出队逻辑来扩展,以支持优先级调度。
  • 对于需要与浏览器渲染同步的任务(如动画),应使用 requestAnimationFrame 来构建调度循环,以获得更平滑的视觉效果 93。scheduler.postTask 提供了更现代、更强大的基于优先级的调度能力 94。

JavaScript 的事件循环模型本身就是一个调度系统,管理着宏任务、微任务和渲染步骤。setTimeout、Promise 和 requestAnimationFrame 等 API 为开发者提供了与这个底层调度系统交互的方式,允许将任务安排在事件循环的不同阶段执行 92。

通过模拟调度器,即使在 JavaScript 的单线程环境中(主线程),也能清晰地展示任务排队、异步执行和协作式让步等核心调度概念。这演示了如何使用基本的语言和运行时特性来实现一种形式的合作式多任务处理 94。

10. 结论

调度器是现代计算系统中不可或缺的核心组件,负责在有限的计算资源(主要是 CPU)上管理和协调并发任务的执行。从操作系统的进程/线程调度,到语言运行时的协程/goroutine/actor 调度,再到数据库查询和网络请求的分发,调度机制无处不在,其根本目标是在相互竞争的任务需求与有限的资源供给之间找到平衡点,以实现特定的系统目标,如效率、公平性、响应速度或满足实时约束。

本次分析探讨了调度器的基本概念、核心职责以及各种实现策略。关键的权衡无处不在:

  • 抢占式 vs. 非抢占式: 在系统响应性和实现复杂性/开销之间的权衡。
  • 静态优先级 vs. 动态优先级: 在简单性/可预测性与自适应性之间的权衡。
  • 通用调度器 vs. 专业化调度器: 在广泛适用性与针对特定领域(如实时、数据库)的优化之间的权衡。
  • 用户级调度 vs. 内核级调度: 在细粒度控制/效率与实现复杂性/阻塞处理难度之间的权衡。

Go 语言的 GMP 模型和 Erlang 的 BEAM 调度器是现代语言运行时如何实现高效 M:N 用户级调度的杰出范例。它们都采用了工作窃取等先进技术来优化负载均衡和可扩展性。然而,它们在内存模型(共享 vs. 隔离)、抢占策略(时间信号 vs. 缩减计数)、垃圾回收(并发全局 vs. per-process)以及错误处理哲学(显式处理 vs. let-it-crash)上的根本差异,深刻地影响了各自调度器的设计和适用场景。Go 更倾向于提供高效的共享内存并发,而 Erlang 则优先考虑隔离性、容错性和软实时性。

随着多核处理器成为主流,以及并发和分布式系统变得日益复杂,调度器的设计和优化变得愈发关键。高效的调度机制是充分发挥硬件潜力、保证应用程序性能、响应性和可扩展性的基石。无论是操作系统开发者、语言运行时设计者,还是构建高并发应用的工程师,深入理解调度器的原理、不同模型的优劣以及它们之间的权衡,对于设计和实现健壮、高效的现代计算系统都至关重要。未来的调度器研究可能会继续探索更智能的自适应策略、更好地利用异构硬件资源以及在分布式环境中实现更优的协调。

Works cited

  1. Process Schedulers in Operating System | GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/process-schedulers-in-operating-system/
  2. Understanding Process Schedulers in Operating Systems - DigiiMento, accessed April 28, 2025, https://digiimento.com/understanding-process-schedulers-in-operating-systems/
  3. What is a scheduler in OS? - Design Gurus, accessed April 28, 2025, https://www.designgurus.io/answers/detail/what-is-a-scheduler-in-os
  4. Scheduling (computing) - Wikipedia, accessed April 28, 2025, https://en.wikipedia.org/wiki/Scheduling_(computing)
  5. Fundamentals of Operating Systems: Process Scheduling Cheatsheet - Codecademy, accessed April 28, 2025, https://www.codecademy.com/learn/fundamentals-of-operating-systems/modules/os-process-scheduling/cheatsheet
  6. 资源调度系统的工作原理是什么 - 亚马逊云科技, accessed April 28, 2025, https://www.amazonaws.cn/what-is/resource-scheduling-system/
  7. Scheduling - OMSCS Notes, accessed April 28, 2025, https://www.omscs-notes.com/operating-systems/scheduling/
  8. Preemptive and Non-Preemptive Scheduling | GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/preemptive-and-non-preemptive-scheduling/
  9. Scheduling In Go : Part I - OS Scheduler - Ardan Labs, accessed April 28, 2025, https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part1.html
  10. CIS 4930/6930: Principles of Cyber-Physical Systems - Chapter 11 Scheduling - University of South Florida, accessed April 28, 2025, https://cse.usf.edu/~haozheng/teach/PrinciplesCPS/slides/chap11-scheduling.pdf
  11. Erlang Scheduler Details and Why It Matters - Hamidreza Soleimani's Blog, accessed April 28, 2025, https://hamidreza-s.github.io/erlang/scheduling/real-time/preemptive/migration/2016/02/09/erlang-scheduler-details.html
  12. Breakdown of Scheduling in Erlang - Mudassar Ali, accessed April 28, 2025, https://mudssrali.com/blog/breakdown-of-scheduling-in-erlang
  13. Multilevel Feedback Queue Scheduling (MLFQ) CPU Scheduling ..., accessed April 28, 2025, https://www.geeksforgeeks.org/multilevel-feedback-queue-scheduling-mlfq-cpu-scheduling/
  14. (PDF) Improving Scheduling Criteria of Preemptive Tasks Scheduled under Round Robin Algorithm using Changeable Time Quantum - ResearchGate, accessed April 28, 2025, https://www.researchgate.net/publication/233966082_Improving_Scheduling_Criteria_of_Preemptive_Tasks_Scheduled_under_Round_Robin_Algorithm_using_Changeable_Time_Quantum
  15. Multilevel Queue (MLQ) CPU Scheduling - GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/multilevel-queue-mlq-cpu-scheduling/
  16. Multilevel feedback queue - Wikipedia, accessed April 28, 2025, https://en.wikipedia.org/wiki/Multilevel_feedback_queue
  17. 8: Scheduling: The Multi-Level Feedback Queue - GitHub Pages, accessed April 28, 2025, https://ceunican.github.io/aos/08.Scheduling_The_Multi-level_Feedback_queue.pdf
  18. Scheduling: The Multi-Level Feedback Queue - Computer Sciences User Pages, accessed April 28, 2025, https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched-mlfq.pdf
  19. Multilevel Feedback Queues (MLFQ) - LASS, accessed April 28, 2025, https://lass.cs.umass.edu/~shenoy/courses/fall14/lectures/Lec05-part2.pdf
  20. Completely Fair Scheduling (CFS) - Steven Gong, accessed April 28, 2025, https://stevengong.co/notes/Completely-Fair-Scheduling
  21. Linux kernel scheduler, accessed April 28, 2025, https://linux-audit.com/kernel/scheduler/
  22. Completely Fair Scheduler - Wikipedia, accessed April 28, 2025, https://en.wikipedia.org/wiki/Completely_Fair_Scheduler
  23. Completely Fair Scheduler and its tuning1, accessed April 28, 2025, https://www.fizyka.umk.pl/~jkob/prace-mag/cfs-tuning.pdf
  24. Understanding the Completely Fair Scheduler (CFS) in Linux - Developer's Coffee, accessed April 28, 2025, https://www.developerscoffee.com/blog/understanding-the-completely-fair-scheduler-cfs-in-linux/
  25. Implementing threads :: Operating systems 2020, accessed April 28, 2025, https://www2.it.uu.se/edu/course/homepage/os/vt20/module-4/implementing-threads/
  26. How is preemptive scheduling implemented for user-level threads in Linux? - Stack Overflow, accessed April 28, 2025, https://stackoverflow.com/questions/20448817/how-is-preemptive-scheduling-implemented-for-user-level-threads-in-linux
  27. GMP - Ginta's blog, accessed April 28, 2025, https://ginta.top/2023/03/22/GMP/
  28. Goroutines vs OS threads - Google Groups, accessed April 28, 2025, https://groups.google.com/g/golang-nuts/c/j51G7ieoKh4/m/wxNaKkFEfvcJ
  29. Go Runtime Scheduler Design Internals - Simplify Complexities - WordPress.com, accessed April 28, 2025, https://freethreads.wordpress.com/2019/01/23/go-runtime-scheduler-design-internals/
  30. Lightweight Preemptive User-Level Threads - Shintaro Iwasaki, accessed April 28, 2025, https://shintaro-iwasaki.github.io/pdfs/papers/PPoPP2021_paper.pdf
  31. Is there preemption with user-level threads - Stack Overflow, accessed April 28, 2025, https://stackoverflow.com/questions/31271523/is-there-preemption-with-user-level-threads
  32. Deep Diving Into the Erlang Scheduler - AppSignal Blog, accessed April 28, 2025, https://blog.appsignal.com/2024/04/23/deep-diving-into-the-erlang-scheduler.html
  33. Go's work-stealing scheduler - rakyll.org, accessed April 28, 2025, https://rakyll.org/scheduler/
  34. Mastering Concurrency: Unveiling the Magic of Go's Scheduler - SAP Community, accessed April 28, 2025, https://community.sap.com/t5/additional-blogs-by-sap/mastering-concurrency-unveiling-the-magic-of-go-s-scheduler/ba-p/13577437
  35. The Go Scheduler: How I Learned to Love Concurrency in 2025 - ByteSizeGo, accessed April 28, 2025, https://www.bytesizego.com/blog/go-scheduler-deep-dive-2025
  36. Thread Scheduling | GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/thread-scheduling/
  37. Go's work-stealing scheduler 新建goroutine 与饥饿模式- papering - 博客园, accessed April 28, 2025, https://www.cnblogs.com/papering/p/17243522.html
  38. Efficient Work-Stealing With Return Barriers - Steve Blackburn, accessed April 28, 2025, https://www.steveblackburn.org/pubs/papers/ws-vee-2014.pdf
  39. Scheduling Parallel Programs by Work Stealing with Private Deques - Arthur Charguéraud, accessed April 28, 2025, https://www.chargueraud.org/research/2013/ppopp/full.pdf
  40. Executing Dynamic Task Graphs Using Work-Stealing - Washington University, accessed April 28, 2025, https://www.cse.wustl.edu/~kunal/resources/Papers/nabbit.pdf
  41. Efficient Synchronization-Light Work Stealing, accessed April 28, 2025, https://crypto.ethz.ch/publications/files/CusPauRit23.pdf
  42. BWoS: Formally Verified Block-based Work Stealing for Parallel Processing - USENIX, accessed April 28, 2025, https://www.usenix.org/system/files/osdi23-wang-jiawei.pdf
  43. Go 语言调度器与Goroutine 实现原理 - 面向信仰编程, accessed April 28, 2025, https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/
  44. Task Scheduler (Concurrency Runtime) - Learn Microsoft, accessed April 28, 2025, https://learn.microsoft.com/en-us/cpp/parallel/concrt/task-scheduler-concurrency-runtime?view=msvc-170
  45. Preemptive scheduling with "do not disturb" - Software Engineering Stack Exchange, accessed April 28, 2025, https://softwareengineering.stackexchange.com/questions/293739/preemptive-scheduling-with-do-not-disturb
  46. Lightweight Preemptive User-Level Threads - Shintaro Iwasaki, accessed April 28, 2025, https://shintaro-iwasaki.github.io/pdfs/papers/PPoPP2021_slides.pdf
  47. (Golang Triad)-I-Understanding the Golang Goroutine Scheduler GPM Model - DEV Community, accessed April 28, 2025, https://dev.to/aceld/understanding-the-golang-goroutine-scheduler-gpm-model-4l1g
  48. Go runtime scheduler - Yousef Meska, accessed April 28, 2025, https://meska54.hashnode.dev/concurrency-in-go-a-deeper-look-into-gos-runtime-scheduler
  49. Go gmp-阿里云, accessed April 28, 2025, https://www.aliyun.com/sswb/712465.html
  50. 32|并发:聊聊Goroutine调度器的原理-Tony Bai · Go语言第一课 - 极客时间, accessed April 28, 2025, https://time.geekbang.org/column/article/476643
  51. Concurrency Models: Go vs Erlang - Jon Eisen, accessed April 28, 2025, https://joneisen.me/programming/2012/12/17/concurrency-models-go-vs-erlang.html
  52. When will Go scheduler create a new M and P? - Stack Overflow, accessed April 28, 2025, https://stackoverflow.com/questions/68312082/when-will-go-scheduler-create-a-new-m-and-p
  53. The Go 1.1 scheduler - Hacker News, accessed April 28, 2025, https://news.ycombinator.com/item?id=6012392
  54. understanding how golang scheduling works - Reddit, accessed April 28, 2025, https://www.reddit.com/r/golang/comments/1j3zi60/understanding_how_golang_scheduling_works/
  55. A Peek Into the Beam, accessed April 28, 2025, https://www.hailelagi.com/writing/a-peek-into-the-beam/
  56. The BEAM Book: Understanding the Erlang Runtime System - Happi, accessed April 28, 2025, https://blog.stenmans.org/theBeamBook/
  57. Designing Concurrent Systems on the BEAM - Happi Hacking, accessed April 28, 2025, https://happihacking.com/blog/posts/2024/designing_concurrency/
  58. Actor4j: A Software Framework for the Actor Model Focusing on the Optimization of Message Passing - UPV, accessed April 28, 2025, https://personales.upv.es/thinkmind/dl/conferences/aict/aict_2018/aict_2018_8_10_10087.pdf
  59. Concurrency Models: Go vs Erlang - Hacker News, accessed April 28, 2025, https://news.ycombinator.com/item?id=5451651
  60. The HiPE/x86 Erlang Compiler: System Description and Performance Evaluation, accessed April 28, 2025, https://www.researchgate.net/publication/225129672_The_HiPEx86_Erlang_Compiler_System_Description_and_Performance_Evaluation
  61. Message Passing and the Actor Model, accessed April 28, 2025, http://dist-prog-book.com/chapter/3/message-passing.html
  62. How the Actor Model Meets the Needs of Modern, Distributed Systems, accessed April 28, 2025, https://doc.akka.io/libraries/akka-core/current/typed/guide/actors-intro.html
  63. Introduction to Actor Model - Ada Beat, accessed April 28, 2025, https://adabeat.com/fp/introduction-to-actor-model/
  64. 1. The Actor Model - Applied Akka Patterns [Book] - O'Reilly, accessed April 28, 2025, https://www.oreilly.com/library/view/applied-akka-patterns/9781491934876/ch01.html
  65. Case Study: Actor Scheduler - 1024cores, accessed April 28, 2025, https://www.1024cores.net/home/scalable-architecture/case-study-actor-scheduler
  66. (PDF) Towards hard real-time Erlang. - ResearchGate, accessed April 28, 2025, https://www.researchgate.net/publication/221211389_Towards_hard_real-time_Erlang
  67. Overview — Erlang System Documentation v28.0-rc2, accessed April 28, 2025, https://www.erlang.org/docs/28/system/design_principles
  68. How to understand real-time operating systems for interviews? - Design Gurus, accessed April 28, 2025, https://www.designgurus.io/answers/detail/how-to-understand-real-time-operating-systems-for-interviews
  69. 实时操作系统- 维基百科,自由的百科全书, accessed April 28, 2025, https://zh.wikipedia.org/zh-cn/%E5%AE%9E%E6%97%B6%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F
  70. Rate-monotonic scheduling - GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/rate-monotonic-scheduling/
  71. CN109308216A - 一种针对不精确计算的单核系统实时任务调度方法 - Google Patents, accessed April 28, 2025, https://patents.google.com/patent/CN109308216A/zh
  72. Real-Time Scheduling Explained: Implementing RMS & EDF with Code Examples, accessed April 28, 2025, https://www.maven-silicon.com/blog/real-time-scheduling/
  73. Earliest deadline first scheduling - Wikipedia, accessed April 28, 2025, https://en.wikipedia.org/wiki/Earliest_deadline_first_scheduling
  74. 最早截止时间优先调度 - 维基百科, accessed April 28, 2025, https://zh.wikipedia.org/zh-cn/%E6%9C%80%E6%97%A9%E6%88%AA%E6%AD%A2%E6%97%B6%E9%97%B4%E4%BC%98%E5%85%88%E8%B0%83%E5%BA%A6
  75. Earliest Deadline First (EDF) CPU scheduling algorithm - GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/earliest-deadline-first-edf-cpu-scheduling-algorithm/
  76. A Simple Guide to Improving Your Skills in Scheduling Queries | Towards Data Science, accessed April 28, 2025, https://towardsdatascience.com/a-simple-guide-to-improve-your-skills-in-scheduling-queries-40df2aea1f24/
  77. 4 Key principles of effective database design - Gleek, accessed April 28, 2025, https://www.gleek.io/blog/database-design-principles
  78. Types of Schedules in DBMS - GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/types-of-schedules-in-dbms/
  79. Self-Tuning Query Scheduling for Analytical Workloads - CMU 15-721 :: Advanced Database Systems (Fall 2024), accessed April 28, 2025, https://15721.courses.cs.cmu.edu/spring2023/papers/07-scheduling/wagner-sigmod21.pdf
  80. The Key Principles for a Smooth-Running Database System - OptimizDBA.com, accessed April 28, 2025, https://optimizdba.com/the-key-principles-for-a-smooth-running-database-system/
  81. 内存事务中并发控制协议研究综述, accessed April 28, 2025, https://crad.ict.ac.cn/fileJSJYJYFZ/journal/article/jsjyjyfz/HTML/2022-04-721.shtml
  82. Scheduling Jobs with Oracle Scheduler - Oracle Help Center, accessed April 28, 2025, https://docs.oracle.com/en/database/oracle/oracle-database/19/admin/scheduling-jobs-with-oracle-scheduler.html
  83. 数据传输的应用场景_数据传输服务(DTS) - 阿里云文档, accessed April 28, 2025, https://help.aliyun.com/zh/dts/product-overview/scenarios
  84. IPVS Scheduling Algorithms — Keepalived 1.4.3 documentation - Read the Docs, accessed April 28, 2025, https://keepalived-pqa.readthedocs.io/en/latest/scheduling_algorithms.html
  85. Cloud Load Balancer scheduling algorithms - Rackspace, accessed April 28, 2025, https://docs.rackspace.com/docs/cloud-load-balancer-scheduling-algorithms
  86. Calculate server loads using Round Robin Scheduling | GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/calculate-server-loads-using-round-robin-scheduling/
  87. Server Load Balancer:SLB scheduling algorithms - Alibaba Cloud, accessed April 28, 2025, https://www.alibabacloud.com/help/en/slb/product-overview/introduction-to-load-balancing-scheduling-algorithm
  88. 1.3. LVS Scheduling Overview | Red Hat Product Documentation, accessed April 28, 2025, https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/4/html/virtual_server_administration/s1-lvs-scheduling-vsa
  89. 什么是并发_并发有哪些应用场景 - 亚马逊云科技, accessed April 28, 2025, https://www.amazonaws.cn/what-is/concurrency/
  90. 极致性价比:自研数据库PolarDB on 自研芯片倚天 - 数据库内核月报, accessed April 28, 2025, http://mysql.taobao.org/monthly/2023/06/03/
  91. 优化环境性能和费用| Cloud Composer, accessed April 28, 2025, https://cloud.google.com/composer/docs/composer-2/optimize-environments?hl=zh-cn
  92. 事件循环:微任务和宏任务 - 现代JavaScript 教程, accessed April 28, 2025, https://zh.javascript.info/event-loop
  93. SetTimeout VS RequestAnimationFrame - GeeksforGeeks, accessed April 28, 2025, https://www.geeksforgeeks.org/settimeout-vs-requestanimationframe/
  94. There are a lot of ways to break up long tasks in JavaScript. | Alex MacArthur, accessed April 28, 2025, https://macarthur.me/posts/long-tasks/
  95. 调度:setTimeout 和setInterval - 现代JavaScript 教程, accessed April 28, 2025, https://zh.javascript.info/settimeout-setinterval
  96. JavaScript 定时任务:精确控制你的代码执行时间原创 - CSDN博客, accessed April 28, 2025, https://blog.csdn.net/weixin_51311218/article/details/132582119
  97. Window: requestAnimationFrame() method - Web APIs | MDN, accessed April 28, 2025, https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame
  98. Unlocking Web Performance with the PostTask Scheduler Web API - Paul Serban, accessed April 28, 2025, https://paulserban.eu/blog/post/unlocking-web-performance-with-the-posttask-scheduler-web-api/

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载