当前位置: 首页 > news >正文

信号量核心机制说明及实际应用(结合ArduPilot代码)

一、信号量的基本概念

信号量本质是一个整数计数器,用于跟踪共享资源的可用数量,或标记某个事件的发生。它通过定义两种原子操作(不可中断)来控制线程对资源的访问:

  • P操作(等待/获取,Acquire):尝试获取资源使用权。若信号量值>0,将其减1并继续执行;若值=0,线程阻塞等待,直到信号量被释放。
  • V操作(释放,Release):释放资源使用权。将信号量值加1,若有线程因等待该信号量而阻塞,唤醒其中一个线程。

信号量的核心作用是:

  1. 互斥(Mutex):确保同一时间只有一个线程访问共享资源(如共享数据、硬件外设)。
  2. 同步:协调多个线程的执行顺序(如线程A必须在事件X发生后才能执行,线程B负责触发事件X)。

二、信号量的两种常见类型

1. 二进制信号量(Binary Semaphore)
  • 值只能为0或1,用于实现互斥(类似“锁”)。
  • 初始值为1:表示资源可用。线程获取时(P操作)值变为0(资源占用),释放时(V操作)值变回1(资源释放)。
  • 应用场景:保护共享数据(如传感器数据、配置参数),避免多线程并发修改导致的数据错乱。
2. 计数信号量(Counting Semaphore)
  • 值可以是任意非负整数,用于控制对有限数量资源的并发访问(如3个缓冲区,最多允许3个线程同时使用)。
  • 初始值为资源总数N:每个线程获取资源时值减1,释放时值加1。当值=0时,后续线程阻塞等待。
  • 应用场景:限制同时访问某资源的线程数量(如网络连接池、IO设备队列)。

三、信号量的核心操作(原子性保证)

信号量的P/V操作必须是原子操作(不可被中断),否则会导致竞态条件(Race Condition)。例如,若两个线程同时执行P操作,可能都判断信号量值>0,导致超过1个线程获取资源。

以二进制信号量为例,操作逻辑如下(伪代码):

// P操作(获取资源)
void sem_wait(Semaphore *sem) {disable_interrupts();  // 关闭中断,确保操作原子性if (sem->value > 0) {sem->value--;      // 资源可用,占用它} else {add_thread_to_wait_queue(sem);  // 资源被占用,线程阻塞等待block_current_thread();         // 切换到其他线程}enable_interrupts();   // 恢复中断
}// V操作(释放资源)
void sem_post(Semaphore *sem) {disable_interrupts();  // 关闭中断,确保操作原子性if (wait_queue_not_empty(sem)) {wake_one_thread_from_queue(sem);  // 唤醒一个等待线程} else {sem->value++;      // 无等待线程,释放资源}enable_interrupts();   // 恢复中断
}

四、在ArduPilot中的实际应用(结合代码)

ArduPilot的ChibiOS调度器中大量使用二进制信号量实现线程同步,以下是典型场景:

1. 保护共享资源(如回调函数列表)
// 信号量定义(用于保护定时器回调列表)
chBSemObjectInit(&_timer_semaphore, false);  // 初始值为0(ChibiOS中false表示初始值0,需先post)// 注册定时器回调(多线程可能并发调用)
void Scheduler::register_timer_process(AP_HAL::MemberProc proc) {chBSemWait(&_timer_semaphore);  // P操作:获取信号量(若被占用则阻塞)// 临界区:修改共享的回调函数列表if (_num_timer_procs < MAX_PROCS) {_timer_proc[_num_timer_procs++] = proc;}chBSemSignal(&_timer_semaphore);  // V操作:释放信号量
}
  • 作用_timer_semaphore确保_timer_proc数组(定时器回调列表)的修改是原子操作,避免多线程同时添加回调导致的数组越界或数据错乱。
  • ChibiOS接口chBSemWait(P操作)、chBSemSignal(V操作)用于二进制信号量。
2. 线程同步(如延迟预期管理)
// 信号量用于保护延迟预期的共享变量
WITH_SEMAPHORE(expect_delay_sem);  // RAII模式:自动获取和释放信号量// 临界区:修改延迟预期的嵌套计数、开始时间等变量
if (ms == 0) {if (expect_delay_nesting > 0) {expect_delay_nesting--;}
} else {expect_delay_start = AP_HAL::millis();expect_delay_nesting++;
}
  • WITH_SEMAPHORE:是ArduPilot封装的RAII(资源获取即初始化)工具,展开后等价于:
    {Semaphore::ScopedLock lock(expect_delay_sem);  // 构造时P操作// 临界区代码
    }  // 析构时自动V操作,确保信号量释放(即使发生异常)
    
  • 作用:保护expect_delay_nesting(延迟嵌套计数)等变量,避免主线程在修改时被其他线程打断,确保延迟预期逻辑的正确性。
3. 防止递归调用(如定时器回调)
void Scheduler::_run_timers() {if (_in_timer_proc) {return;  // 若已在执行回调,直接返回(防止递归)}_in_timer_proc = true;  // 标记正在执行// 临界区:执行所有注册的定时器回调for (int i = 0; i < _num_timer_procs; i++) {_timer_proc[i]();}_in_timer_proc = false;  // 标记执行结束
}
  • 结合信号量:虽然这里用标志位_in_timer_proc防止递归,但本质上与信号量的互斥逻辑一致——确保同一时间只有一个线程执行回调列表,避免回调函数内部再次触发_run_timers导致的栈溢出或数据冲突。

五、信号量的常见问题与解决方案

1. 死锁(Deadlock)
  • 现象:线程A持有信号量S1并等待S2,线程B持有S2并等待S1,双方永久阻塞。
  • 举例
    // 线程A
    chBSemWait(S1);
    chBSemWait(S2);  // 等待S2(被线程B持有)// 线程B
    chBSemWait(S2);
    chBSemWait(S1);  // 等待S1(被线程A持有)
    
  • 解决方案
    • 按固定顺序获取信号量(如先S1后S2)。
    • 限制信号量持有时间(超时后释放并重试)。
    • 使用ChibiOS的chSemWaitTimeout(带超时的P操作)。
2. 优先级反转(Priority Inversion)
  • 现象:低优先级线程持有信号量,高优先级线程因等待该信号量而被阻塞,导致高优先级任务延迟。
  • 举例
    • 低优先级线程L持有信号量S,正在执行。
    • 中优先级线程M抢占L的CPU(因优先级更高),导致L无法释放S。
    • 高优先级线程H等待S,却因M的抢占而迟迟无法执行(H优先级 > M > L,但H被L间接阻塞)。
  • 解决方案
    • 优先级继承:ChibiOS支持“优先级继承协议”,当低优先级线程持有高优先级线程等待的信号量时,临时将低优先级线程提升到高优先级,确保其尽快释放资源。
    • 缩短临界区:减少信号量的持有时间(如临界区仅包含必要操作)。
3. 性能开销
  • 信号量操作涉及原子指令和可能的线程切换,频繁调用会增加CPU负担。
  • 优化
    • 减少临界区大小(仅保护必要代码)。
    • 对只读共享数据,可使用“无锁读取”(如复制到线程本地缓存)。
    • 在实时性要求极高的场景(如传感器采样),使用中断屏蔽替代信号量(短期禁用中断,避免线程切换)。

信号量是多线程编程中解决同步与互斥问题的核心工具,其核心价值在于:

  1. 互斥保护:确保共享资源(如数据、硬件)同一时间仅被一个线程访问,避免数据竞争。
  2. 时序协调:控制线程执行顺序,满足实时系统中“事件驱动”的需求(如传感器数据就绪后再执行姿态解算)。
http://www.dtcms.com/a/276367.html

相关文章:

  • C++类模版2
  • 人工智能大语言模型提供了一种打败小朋友十万个为什么的捷径
  • 附件1.2025年世界职业院校技能大赛赛道简介
  • 1. JVM介绍和运行流程
  • 计算机毕业设计springboot的零食推荐系统 基于SpringBoot的在线零食商城个性化推荐平台 JavaWeb驱动的智能零食选购与推荐系统
  • HT8313功放入门
  • 【论文阅读】HCCF:Hypergraph Contrastive Collaborative Filtering
  • 创建uniapp项目引入uni-id用户体系使用beforeRegister钩子创建默认昵称
  • Pandas-数据加载与保存
  • Can201-Introduction to Networking: Application Layer应用层
  • 深入解析 Stack 和 Queue:从原理到实战应用
  • 【读书笔记】从AI到Transformer:LLM技术演进全解析
  • 推荐系统-Random算法
  • jieba 库:中文分词的利器
  • 【Lucene/Elasticsearch】**Query Rewrite** 机制
  • day68—DFS—二叉树的所有路径(LeetCode-257)
  • 微信小程序form组件的使用
  • 从json中提取i18n字段
  • nodej获取当前系统的cpu架构信息
  • 程序员软技能之推广营销-04-长尾效应(Long Tail Effect)
  • UnityShader——SSAO
  • C++类模版1
  • Linux进程的生命周期:状态定义、转换与特殊场景
  • 【Elasticsearch】检索模板(Search Template)
  • 从输入到路径:AI赋能的地图语义解析与可视化探索之旅(2025空间智能全景)
  • C++ Boost 自动链接机制详解
  • Java基础:泛型
  • 单调栈单调队列【算法进阶】
  • 11. JVM中的分代回收
  • 数据结构自学Day6 栈与队列