线程同步概念

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

255.2.6线程同步概念

本节没有新增或修改应用层业务代码。视频中展示的是通过全局搜索查看 RT-Thread 5.0.2 的 rt_thread_yield()、rt_enter_critical() 和 rt_exit_critical() 等内核实现;按照“只收录课堂业务代码、不抄入库函数源码”的要求,标题下不放置源码块。

一、线程同步概念

1.1 线程同步的概念

裸机程序通常以一个主循环配合中断运行;引入 RTOS 后,多个线程会在调度器控制下交替执行。即使本课程使用的是单核 STM32,同一时刻只有一个线程真正占用 CPU,线程仍可能在任意调度点被切换,因此多步操作之间可能穿插其他线程或中断的操作。

当多个执行流共享变量、缓冲区或外设时,如果没有约定访问次序和访问权限,就可能出现数据被覆盖、读取到半更新状态或业务步骤前后颠倒等问题。线程同步机制正是用来协调这些执行流的。

1.1.1 线程同步

线程同步是指多个线程借助特定机制建立执行次序或协作关系,使它们在满足条件时继续运行、在条件未满足时等待。同步并不表示“几个线程在同一时刻做同一件事”,也不仅仅表示固定为“线程 2 → 线程 1 → 线程 3”;它还包括资源就绪通知、生产者与消费者配合、等待多个事件等情形。

需要分清两个相近概念:

概念关注的问题示例
同步(synchronization)谁应当先做、什么条件满足后谁才能继续采集线程写完一帧数据后,显示线程才开始读取
互斥(mutual exclusion)某段时间内谁拥有共享资源的独占访问权多个线程访问同一个串口发送缓冲区时,只允许一个线程完成整帧操作

互斥可以看成线程同步的一种特殊形式。RT-Thread 本章重点介绍的同步对象包括信号量、互斥量和事件集:

  • 信号量可以表示通知、令牌数量或可用资源数量;
  • 互斥量专门用于线程之间对共享资源的互斥访问;
  • 事件集用多个事件位表达一个或多个条件,可按“任一条件”或“所有条件”组合等待。

原笔记把“相同优先级线程的执行顺序”写成随机并不准确。RT-Thread 是基于优先级的抢占式调度器:更高优先级就绪线程优先运行;同优先级线程可按照就绪队列和时间片轮转。实际先后还受线程启动顺序、阻塞与唤醒时刻、系统 tick 和中断影响,因此不能依赖一次观察到的自然先后顺序表达业务约束;需要确定次序时,必须使用同步对象明确建立关系。

1.1.2 临界区

临界区(critical section)是访问共享资源、并且不能与其他冲突访问交错执行的一段代码。共享资源可能是:

  • 多个线程都会读写的全局变量、链表或环形缓冲区;
  • 由多条指令共同维护的一组数据不变量;
  • 串口、I²C、SPI 等不能被多个调用者任意穿插操作的外设;
  • 线程和中断服务程序共同访问的状态。

“临界区”强调的是访问共享资源的代码范围,不是说代码本身天然具有保护能力。只有所有竞争者都遵守同一种保护规则,互斥才会成立。

竞争条件(race condition)是指程序结果依赖不可预测的执行交错。例如两个线程同时对共享计数器执行“读取旧值 → 加一 → 写回”时,两者可能读到相同旧值,最后只增加一次。单条 C 语句也可能编译成多条机器指令,所以不能仅凭“代码只有一行”就认定操作具有原子性。

还要纠正“临界区代码不能被抢占”这一过度概括:

  • 使用关中断或锁调度器保护时,当前线程在相应范围内确实不会按普通路径被其他线程抢占;
  • 使用互斥量或二值信号量保护时,持锁线程仍可能被与该资源无关的更高优先级线程抢占,但其他遵守同一把锁的线程无法同时进入受保护区域,资源的一致性仍然成立。

因此,临界区真正要求的是“冲突访问不能重叠”,并非任何场景都要求 CPU 从头到尾绝不切换线程。

1.1.3 临界区的保护方式

课程列出三类保护方式。它们阻止的对象、代价和适用范围并不相同:

方式能阻止什么中断是否继续响应典型用途与限制
关闭中断当前单核 CPU 上的线程切换和受屏蔽中断对共享数据的并发访问否最短的底层原子操作;会增加中断延迟,必须极短
禁止调度其他线程抢占当前线程是只防线程竞争,不能防 ISR 访问;临界区内不能执行会阻塞的操作
信号量/互斥量其他遵守同一同步对象的线程进入受保护区域是等待者可挂起,不必全程占住 CPU;适合较长的线程级资源访问

无论采用哪一种方式,都应保证进入和退出严格配对,并让保护范围覆盖完整的一致性操作。范围过小仍会发生竞争,范围过大会增加中断延迟、调度延迟或其他线程的等待时间。

1.1.3.1 关闭中断

RT-Thread 使用以下两个底层接口保存并恢复中断状态:

  • rt_hw_interrupt_disable():屏蔽当前端口规定的可屏蔽中断,并返回调用前的中断状态;
  • rt_hw_interrupt_enable(level):用传入的 level 恢复先前状态,不能简单理解为无条件“打开所有中断”。

典型逻辑是“保存旧状态 → 关中断 → 修改共享内核状态 → 恢复旧状态”。保存并恢复旧状态非常重要:如果调用前中断本来就是关闭的,退出内层临界区时不应错误地提前开启中断。

视频回看了 rt_thread_yield() 的内核实现。该函数先取得当前线程,再调用 rt_hw_interrupt_disable();在中断关闭期间重装当前线程的剩余时间片并设置主动让出标志;随后用保存的 level 恢复中断,最后才调用 rt_schedule()。这里的关中断只保护极短的调度状态更新,不是让整个线程业务都在关中断状态下执行。

关闭中断的影响是全局性的:受屏蔽中断不能及时响应,由这些中断触发的系统 tick、外设服务和抢占调度也会被推迟。因此应遵守以下原则:

  • 临界区只保留必要的几条操作,不做打印、延时、等待 IPC 或大数据处理;
  • 退出时传回原先保存的 level;
  • 若共享数据还会被 ISR 访问,线程级互斥量不能代替中断保护;
  • 在多核系统中,关闭本地 CPU 中断并不能阻止另一核心并发访问,还需要自旋锁等跨核机制。本课程 STM32F103 单核工程不涉及这一分支。
1.1.3.2 禁止调度

视频思维导图和配套 RT-Thread 5.0.2 源码中的接口是:

  • rt_enter_critical():锁住线程调度器;
  • rt_exit_critical():解除一层调度锁,并在最外层退出时按需要重新调度。

原笔记中的 rt_thread_scheduler_lock() / rt_thread_scheduler_unlock() 不是本节实际展示的接口,应以上述名称为准。

配套单核源码用 rt_scheduler_lock_nest 记录嵌套层数。每次 rt_enter_critical() 将层数加一,每次 rt_exit_critical() 将层数减一;只有降到零才真正恢复调度。这使同一线程中的嵌套调用可以成对工作,但也意味着少调用一次退出函数就会让调度器长期保持锁定。

锁调度器期间,硬件中断仍然可以响应,所以这种方式与关中断有两个关键区别:

  • 它能阻止普通线程切换,却不能阻止 ISR 修改同一份数据;
  • 它对中断实时性的影响较小,但会推迟其他线程运行。

调度被锁定时不能调用会使当前线程阻塞或挂起的接口,例如延时、永久等待信号量或等待消息。否则当前线程要求睡眠,调度器却又不允许选择其他线程,容易造成断言失败或系统失去正常调度。调度锁同样只适合短小、确定不会阻塞的线程级临界区。

1.1.3.3 互斥特性保护临界区

线程级锁的基本使用顺序是:

  1. 进入临界区前获取同步对象;
  2. 获取成功后访问共享资源;
  3. 在所有正常和异常退出路径上释放同步对象;
  4. 其他线程获取不到时,根据接口参数立即返回、限时等待或挂起等待。

二值信号量和互斥量都能形成“一次只让一个线程进入”的效果,但二者不能视为完全相同:

  • 二值信号量本质上仍是无所有权的计数/通知对象,可以由不同执行流获取和释放;
  • 互斥量有明确所有者,只能由持有它的线程释放,并具有递归持有和优先级继承等资源锁语义。

使用线程级锁时,持有者可以被无关的高优先级线程抢占;真正被阻止的是其他试图获取同一个锁的线程。与长时间关中断相比,这通常能保留更好的中断响应和系统可调度性。

要避免几个常见错误:获取后忘记释放、错误路径提前返回而漏释放、以相反顺序获取多把锁导致死锁,以及在持锁期间执行无界耗时或永久等待操作。

1.1.4 信号量

信号量(semaphore)可以理解为“可用令牌的计数器 + 等待队列”。在 RT-Thread 中:

  • 当前值大于 0 时,获取操作成功并把计数减一;
  • 当前值等于 0 时,调用者可按等待参数选择立即返回、等待指定 tick 或永久等待;
  • 释放时,如果已有线程正在等待,则优先唤醒等待线程;如果没有等待线程,计数加一。

等待时间的单位是 OS tick,不是固定的毫秒。只有结合工程的 RT_TICK_PER_SECOND,才能把 tick 换算为实际时间。

课程用“厕所”和“停车场”帮助理解,但应把模型对应准确:

用法常见初值含义
同步通知0条件尚未发生,等待线程先阻塞;生产者或 ISR 完成工作后释放一次,通知等待者继续
二值锁1只有一个令牌;成功获取后变为 0,其他线程等待,使用完再释放
资源计数N有 N 个同类资源实例;每获取一个资源就减一,归还后加一

RT-Thread 内核只有通用计数信号量对象,并没有另一个会把数值硬性限制为 0/1 的独立“二值信号量”类型。所谓二值信号量,是应用按 0/1 规则使用普通信号量;如果没有等待者却连续多次释放,其计数仍可能超过 1。因此把它当锁时必须保证获取与释放严格匹配。

停车场初值为 5,表示五个可独立占用的车位,最多允许五个使用者各自占用一个资源;它不表示可以让五个线程同时进入同一段非可重入代码。若真正共享的资源只有一个,就应使用初值为 1 的锁或互斥量。

信号量没有所有权:释放者可以不是先前的获取者,这使它很适合“线程等待、另一个线程或 ISR 发出通知”的场景;但也正因为没有所有权,它不具备互斥量的递归持有和优先级继承能力。用于保护普通线程共享资源时,通常优先考虑互斥量;用于通知、线程与中断同步或资源计数时,信号量更自然。

1.1.5 互斥量

互斥量(mutex)是面向线程共享资源保护的内核对象。课程中的 mtx 只是 mutex 常用的名称缩写,可作为变量名或对象名理解,不是一个决定行为的“标志位”。

它的工作过程是:线程进入临界区前获取互斥量;获取成功后成为所有者;其他线程再获取同一个互斥量时会等待;所有者完成资源访问后释放,等待者才有机会继续。

互斥量和二值信号量都能呈现“锁定/解锁”的外观,但 RT-Thread 5.0.2 中的重要区别如下:

特性二值信号量用作锁互斥量
所有权无有,记录持有线程
谁能释放不要求必须是获取者只能由所有者线程释放
同一线程重复获取会因令牌已用完而等待,可能自锁支持递归获取,并记录持有次数;必须对应次数地释放
优先级反转保护无支持优先级继承,必要时临时提升持锁线程优先级
ISR 中使用可用信号量释放操作通知线程互斥量依赖线程所有权,不能在 ISR 中获取或释放
更适合的用途通知、计数;简单场景也可互斥普通线程之间保护唯一共享资源

优先级继承用于缓解经典优先级反转:低优先级线程持有互斥量、高优先级线程等待它时,内核临时提高持锁线程的优先级,使其尽快完成临界区并释放资源,避免被无关的中优先级线程长期抢占。它不能修复死锁或无限期持锁,所以互斥量仍应尽早释放。

选择保护方式时可以先问两个问题:

  1. 共享数据是否会被 ISR 访问?如果会,需要极短的中断保护或专门的中断安全设计,不能只加线程互斥量。
  2. 只是线程之间保护一个共享资源,还是要传递通知/统计资源数量?前者优先考虑互斥量,后者优先考虑信号量或事件集。

核对依据:本课程视频与配套工程中的 RT-Thread 5.0.2 rt-thread/src/thread.c、rt-thread/src/scheduler_up.c、rt-thread/src/ipc.c 和 rt-thread/include/rtdef.h,以及 RT-Thread 官方线程间同步文档、官方中断管理文档、RT-Thread v5.0.2 thread.c、RT-Thread v5.0.2 scheduler_up.c 和 RT-Thread v5.0.2 ipc.c。