Skip to content

1.7 信号量与互斥锁 ​

这节要搞懂:两个任务同时动一个东西会出什么事,怎么防。 需要基础:1.6 ⭐ 本章重点

一、先看会出什么事 ​

有个全局变量 counter,两个任务都在给它加 1:

c
int counter = 0;

// 任务 A 和任务 B 都执行这一句
counter++;

你以为加了两次就是 2?不一定。

因为 counter++ 在芯片里其实是三步:

① 把 counter 读出来   (读到 0)
② 加 1                (算出 1)
③ 写回去              (写入 1)

如果任务 A 做完 ① 就被抢占了,任务 B 完整做完三步(counter 变成 1), 然后任务 A 回来继续做 ②③ —— 它手里还是老数据 0,加 1 写回去还是 1。

加了两次,结果是 1。数据丢了。

二、什么是竞态 ​

竞态(Race Condition)指的是:多个任务同时访问同一份共享资源,最终结果取决于它们的执行顺序,从而导致不可预期的错误。

只要多个任务会碰同一个东西(变量、屏幕、串口、传感器),就必须保护。这是多任务编程最大的坑。

FreeRTOS 提供了两类工具来解决它:互斥锁和信号量。

三、什么是互斥锁 ​

互斥锁(Mutex,Mutual Exclusion)是一种保护共享资源的机制。它保证同一时刻只有一个任务能访问被保护的资源,其他任务必须等待。

打个比方:互斥锁就像一间单人厕所的钥匙。

  • 谁拿到钥匙谁进去
  • 别人只能在外面等
  • 出来必须把钥匙还回去
c
SemaphoreHandle_t counter_mutex;              // 定义一把锁

void app_init(void)
{
    counter_mutex = xSemaphoreCreateMutex();  // 创建(配一把钥匙)
}

void task_add(void *pvParameters)
{
    while (1) {
        // 拿钥匙,最多等 100 毫秒
        if (xSemaphoreTake(counter_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {

            counter++;                        // 安全了,这段时间只有我能动

            xSemaphoreGive(counter_mutex);    // ⚠️ 一定要还钥匙!
        } else {
            printf("take mutex timeout\n");   // 等太久了,这次先不改
        }

        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

忘了还钥匙

xSemaphoreGive 忘了写 → 别人永远拿不到 → 整个程序卡死。

写 Take 的时候就把 Give 一起写好,养成习惯。

四、什么东西需要上锁 ​

需要上锁 ✅不需要上锁 ❌
多个任务都会改的全局变量只在一个任务内部用的局部变量
屏幕、串口、I2C 这类共享硬件只有一个任务会碰的硬件

五、什么是信号量 ​

信号量(Semaphore)是一种任务间的同步与资源计数机制。它内部维护一个计数值:任务"获取"(Take)时计数减一,减到 0 就得等待;"释放"(Give)时计数加一。

换句话说:信号量本质上就是一个带等待功能的计数器。

按这个计数值的最大值,信号量分成两种:

类型计数范围用来干什么
二值信号量0 或 1通知:某件事发生了
计数信号量0 ~ N管理 N 份资源:还剩几个可用

二值信号量其实是计数信号量的特例

最大值为 1 的计数信号量,就是二值信号量。

六、二值信号量:用来"通知" ​

二值信号量(Binary Semaphore)的计数值只有 0(没信号)和 1(有信号) 两种状态,用于表达"某件事发生了没有"。

打个比方:二值信号量就是一个铃。服务员按一下铃,后厨听到就出来端菜。铃不响,后厨就安静等着。

最典型的场景:按键触发中断,中断里不能干重活,就"按个铃"通知任务来处理。

c
SemaphoreHandle_t key_sem;

void app_init(void)
{
    key_sem = xSemaphoreCreateBinary();          // 创建二值信号量
}

// 中断服务函数:按铃就走,绝不停留
void IRAM_ATTR key_isr_handler(void *arg)
{
    xSemaphoreGiveFromISR(key_sem, NULL);        // 注意函数名带 FromISR
}

// 任务:一直等铃响
void task_key(void *pvParameters)
{
    while (1) {
        // 铃不响就一直睡,不占 CPU
        xSemaphoreTake(key_sem, portMAX_DELAY);

        handle_key();                            // 铃响了才干活
    }
}

中断里的规矩

在中断服务函数里,只能用名字带 FromISR 的版本,而且不能等待。用错了直接崩溃。

记法:中断里只按铃,不干活。

七、计数信号量:用来"管理 N 份资源" ​

计数信号量(Counting Semaphore)的计数值可以是 0 到 N,表示当前还有几份资源可用。每取走一份计数减一,归还一份计数加一,减到 0 时后来者必须等待。

打个比方:计数信号量就是一个有 3 个车位的停车场。

  • 一辆车进去 → 剩余车位 3 → 2
  • 又进两辆 → 剩余 0
  • 第四辆车到了 → 只能在门口等,直到有车开走
c
SemaphoreHandle_t slot_sem;

void app_init(void)
{
    // 参数:最大值 3,初始值 3(一开始 3 个车位全空)
    slot_sem = xSemaphoreCreateCounting(3, 3);
}

void task_worker(void *pvParameters)
{
    while (1) {
        // 抢一个车位,没位置就等着
        if (xSemaphoreTake(slot_sem, portMAX_DELAY) == pdTRUE) {

            do_heavy_work();                     // 最多只有 3 个任务同时干这件事

            xSemaphoreGive(slot_sem);            // 开走,把车位还回去
        }
    }
}

常见用途:

场景计数值代表什么
限制同时联网的任务数还能同时发几个请求
管理一组缓冲区还有几块缓冲区空着
生产者-消费者已经生产了几件待处理

新手先了解就行

计数信号量在小项目里用得不多,我们的天气时钟一次都没用到。

知道"它是能数到 N 的信号量"就够了,遇到需要限流的场合再回来看。

八、三者对比 ⭐ ​

互斥锁二值信号量计数信号量
用来干嘛保护共享资源通知某件事发生管理 N 份资源
计数范围0 或 10 或 10 ~ N
谁拿谁还必须同一个任务A 给、B 拿,随便随便
创建函数xSemaphoreCreateMutex()xSemaphoreCreateBinary()xSemaphoreCreateCounting(N, N)
典型场景保护屏幕、全局变量中断通知任务限制并发数量

互斥锁和二值信号量长得像,但别混用

两者计数范围都是 0/1,但用途完全不同:

  • 保护资源用互斥锁 —— 它要求"谁拿谁还",而且内部有额外机制避免低优先级任务长期占着锁不放
  • 通知用二值信号量 —— 它允许"中断给、任务拿",本来就不是同一个人

记住:保护用锁,通知用信号量。

通知还有一个更省的选择

如果只是通知某一个特定的任务,FreeRTOS 还提供了比信号量更快、更省内存的 任务通知。

1.8 会介绍它,以及什么时候该用它、什么时候还是得用信号量。

本节要点 ​

  • 竞态 = 多个任务抢同一份资源导致结果出错
  • 互斥锁 = 保证同一时刻只有一个任务能访问,Take 之后一定要 Give
  • 信号量本质是一个带等待功能的计数器,分两种:
    • 二值信号量(0/1):用来通知,最常用
    • 计数信号量(0~N):用来管理 N 份资源,新手用得少
  • 中断里只能用 GiveFromISR,只按铃、不干活
  • 保护用锁,通知用信号量

动手试试 ​

  1. 两个任务各自给同一个变量加 10 万次,不加锁,打印最终结果,看看少了多少。
  2. 加上互斥锁再跑一次,验证结果正确了。
  3. 故意把 xSemaphoreGive 注释掉,观察程序怎么卡死。
  4. 创建一个初始值为 2 的计数信号量,让 4 个任务去抢,打印它们进入和离开的时刻,观察是不是永远最多只有 2 个同时在里面。