线程同步_事件集

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

258.2.9线程同步_事件集

// ==================== applications/event_test.c ==================== // 本段是课程复制并适配到 RT-Thread 5.0.2 的事件集例程
#include <rtthread.h> // 引入 RT-Thread 内核、线程、事件集和 MSH 命令导出接口
#define THREAD_PRIORITY      9 // 定义基准线程优先级;RT-Thread 中数值越小,优先级越高
#define THREAD_TIMESLICE     5 // 定义两个示例线程的时间片为 5 个 OS tick
#define EVENT_FLAG3 (1 << 3) // 定义第 3 号事件位:二进制位索引为 3,数值为 0x08
#define EVENT_FLAG5 (1 << 5) // 定义第 5 号事件位:二进制位索引为 5,数值为 0x20
static struct rt_event event; // 分配一个静态事件集控制块,只允许本 C 文件访问
rt_align(RT_ALIGN_SIZE) // 要求紧随其后的线程栈首地址按 RT_ALIGN_SIZE 对齐
static char thread1_stack[1024]; // 为静态线程 thread1 分配 1024 字节栈空间
static struct rt_thread thread1; // 分配静态线程 thread1 的线程控制块
static void thread1_recv_event(void *param) // 定义 thread1 入口函数,负责接收事件
{ // thread1_recv_event 函数体开始
    rt_uint32_t e; // 保存本次实际接收到的 32 位事件掩码
    if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5), // 等待 event 中的事件 3 或事件 5
                      RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, // 任一位满足即可,并在接收后清除对应事件位
                      RT_WAITING_FOREVER, &e) == RT_EOK) // 永久等待,把实际收到的位写入 e,并判断是否成功
    { // 第一次事件接收成功分支开始
        rt_kprintf("thread1: OR recv event 0x%x\n", e); // 以十六进制打印 OR 模式收到的事件掩码
    } // 第一次事件接收成功分支结束
    rt_kprintf("thread1: delay 1s to prepare the second event\n"); // 提示 thread1 将延时 1 秒再进行第二次接收
    rt_thread_mdelay(1000); // 让 thread1 延时约 1000 ms,期间事件位仍可被发送并保留
    if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5), // 第二次仍关注事件 3 和事件 5
                      RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, // 要求两位都已置位,并在接收后清除它们
                      RT_WAITING_FOREVER, &e) == RT_EOK) // 永久等待 AND 条件满足,并把结果写入 e
    { // 第二次事件接收成功分支开始
        rt_kprintf("thread1: AND recv event 0x%x\n", e); // 打印同时满足的事件掩码,本例应为 0x28
    } // 第二次事件接收成功分支结束
    rt_kprintf("thread1 leave.\n"); // 提示 thread1 的一次性入口函数即将返回
} // thread1_recv_event 函数体结束
rt_align(RT_ALIGN_SIZE) // 要求紧随其后的 thread2 栈首地址按 RT_ALIGN_SIZE 对齐
static char thread2_stack[1024]; // 为静态线程 thread2 分配 1024 字节栈空间
static struct rt_thread thread2; // 分配静态线程 thread2 的线程控制块
static void thread2_send_event(void *param) // 定义 thread2 入口函数,负责依次发送事件
{ // thread2_send_event 函数体开始
    rt_kprintf("thread2: send event3\n"); // 提示即将第一次发送事件 3
    rt_event_send(&event, EVENT_FLAG3); // 把事件集的第 3 号事件位置 1
    rt_thread_mdelay(200); // 延时约 200 ms 后继续发送
    rt_kprintf("thread2: send event5\n"); // 提示即将发送事件 5
    rt_event_send(&event, EVENT_FLAG5); // 把事件集的第 5 号事件位置 1
    rt_thread_mdelay(200); // 再延时约 200 ms
    rt_kprintf("thread2: send event3\n"); // 提示将再次发送事件 3
    rt_event_send(&event, EVENT_FLAG3); // 再次把第 3 号事件位置 1,使后续 AND 条件具备两位
    rt_kprintf("thread2 leave.\n"); // 提示 thread2 的一次性入口函数即将返回
} // thread2_send_event 函数体结束
int event_sample(void) // 定义 MSH 命令调用的示例入口函数
{ // event_sample 函数体开始
    rt_err_t result; // 保存 RT-Thread 接口返回的错误状态
    result = rt_event_init(&event, "event", RT_IPC_FLAG_PRIO); // 初始化静态事件集,等待线程按优先级排列
    if (result != RT_EOK) // 判断静态事件集是否初始化失败
    { // 事件集初始化失败分支开始
        rt_kprintf("init event failed.\n"); // 输出事件集初始化失败提示
        return -1; // 返回 -1,停止继续初始化线程
    } // 事件集初始化失败分支结束
    rt_thread_init(&thread1, // 初始化调用者提供控制块的静态线程 thread1
                   "thread1", // 设置线程名称为 thread1
                   thread1_recv_event, // 指定接收事件的线程入口函数
                   RT_NULL, // 不向线程入口函数传递参数
                   &thread1_stack[0], // 指定 thread1_stack 的首地址作为线程栈起点
                   sizeof(thread1_stack), // 指定 thread1 栈大小为整个 1024 字节数组
                   THREAD_PRIORITY - 1, THREAD_TIMESLICE); // 设置优先级为 8、时间片为 5 tick
    rt_thread_startup(&thread1); // 启动 thread1;它会先等待事件 3 或事件 5
    rt_thread_init(&thread2, // 初始化调用者提供控制块的静态线程 thread2
                   "thread2", // 设置线程名称为 thread2
                   thread2_send_event, // 指定发送事件的线程入口函数
                   RT_NULL, // 不向线程入口函数传递参数
                   &thread2_stack[0], // 指定 thread2_stack 的首地址作为线程栈起点
                   sizeof(thread2_stack), // 指定 thread2 栈大小为整个 1024 字节数组
                   THREAD_PRIORITY, THREAD_TIMESLICE); // 设置优先级为 9、时间片为 5 tick
    rt_thread_startup(&thread2); // 启动 thread2,使其按顺序发送三个事件通知
    return 0; // 示例对象和线程均已初始化,向 MSH 返回成功
} // event_sample 函数体结束
MSH_CMD_EXPORT(event_sample, event sample); // 把 event_sample 导出为同名 MSH 命令,并附加帮助说明
// ==================== applications/main.c ==================== // 本段是课程最终工程中的主线程业务代码
#include <rtthread.h> // 引入 RT-Thread 的主线程和毫秒延时接口
#define DBG_TAG "main" // 设置 rtdbg 日志标签为 main
#define DBG_LVL DBG_LOG // 设置日志输出级别
#include <rtdbg.h> // 引入 RT-Thread 调试日志宏
int main(void) // RT-Thread 用户主线程入口函数
{ // main 函数体开始
    int count = 1; // 初始化循环变量,课程源码用它维持主线程循环
    while (count++) // count 非 0 时持续执行;本例通常不会主动退出
    { // 主线程循环体开始
        //LOG_D("Hello RT-Thread!"); // 课程把周期日志注释掉,避免干扰 MSH 示例输出
        rt_thread_mdelay(1000); // 主线程每次延时约 1000 ms,让其他线程和 shell 运行
    } // 主线程循环体结束
    return RT_EOK; // 理论上的正常返回值;本例通常不会执行到这里
} // main 函数体结束

源码说明: 上方依次收录课程配套工程最终的 applications/event_test.c 与 applications/main.c 业务代码。原文件说明性注释被替换成逐行初学者注释,文件内的变量、宏、函数、参数、调用顺序、字符串和被注释掉的 LOG_D 均保持课程原样;没有把 RT-Thread 的事件集、线程或 MSH 库实现抄入笔记。

一、事件集

事件集(event set)是一组由单独比特表示的事件状态。某个条件发生时,发送方把对应比特置 1;接收线程可以等待“任意一个关注事件发生”或“全部关注事件都发生”,条件满足后继续运行。

本节使用的 RT-Thread 5.0.2 中,struct rt_event 的核心状态是一个 rt_uint32_t set,因此每个事件集对象包含 32 个事件位,位索引为 0~31。等待该对象的线程会在自己的线程控制块中记录“关注哪些位”和“使用 AND 还是 OR”,但不能把它误解成每个线程天然私有一份独立的 32 位事件集。

事件集适合表达“条件是否已经发生”,不携带业务数据:

  • 每个比特代表一种由应用自行约定的事件,例如“按键按下”“DMA 完成”“网络已连接”。
  • 一个线程可等待多个事件,一个事件也可使多个满足条件的等待线程恢复,实现一对多、多对多同步。
  • 事件位只有 0/1 状态,没有次数队列。同一位在清除前被连续发送多次,结果仍只是 1。
  • 需要保存发生次数时应使用计数信号量;需要传输数据时应使用邮箱、消息队列或其他数据通道。

1.1 事件集工作机制

可以把事件集想象成一个 32 位开关面板:

bit31                                      bit0
  0  ...  0  [事件5] 0 [事件3] 0 0 0
                  ↑        ↑
                0x20     0x08

线程用掩码指出自己关注哪些开关,再选择接收关系:

接收选项满足条件适合场景
RT_EVENT_FLAG_OR关注位中任意一位为 1多个条件满足其一即可继续,属于独立同步
RT_EVENT_FLAG_AND关注位全部为 1多个条件全部就绪后才能继续,属于关联同步
RT_EVENT_FLAG_CLEAR与 OR/AND 组合使用接收成功后清除相应事件位,使后续接收等待新事件

“AND 要求同时发生”更准确的含义是:内核检查时,所有目标位必须同时保持为 1。这些事件不必在同一个瞬间产生;只要早先置位的事件尚未清除,后续事件到来后也能组成 AND 条件。

发送事件的本质是按位或:

event.set = event.set | send_mask

所以连续两次发送同一个 0x08 不会得到 0x10,而仍是 0x08。这就是“事件无排队性”或“不累计次数”的含义。

接收成功时,recved 返回本次匹配到的事件位。若设置了 CLEAR,内核再清除相应位;若不设置,事件位会继续保持为 1,下一次相同条件的接收可能立即成功。

1.2 事件集的管理方式

事件集同样有动态与静态两套生命周期:

对象形式创建/初始化删除/脱离存储来源
动态事件集rt_event_create()rt_event_delete()内核动态分配
静态事件集rt_event_init()rt_event_detach()调用者提供 struct rt_event

接口必须正确配对。删除或脱离事件对象会唤醒其上的等待线程;销毁前仍应先确保其他执行路径不会继续访问该句柄。本例声明 static struct rt_event event,然后调用 rt_event_init(),属于静态对象。

初始化事件集
rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag);
  • event:调用者提供的事件控制块地址。
  • name:内核对象名。
  • flag:多个线程等待时的队列规则,RT_IPC_FLAG_PRIO 按线程优先级排列,RT_IPC_FLAG_FIFO 按等待先后排列。
  • 初始化后 event->set = 0,即所有事件位都尚未发生。

课程最终代码选择 RT_IPC_FLAG_PRIO。这里与上一节互斥量不同:RT-Thread 5.0.2 的事件集确实保留并使用 FIFO/PRIO 参数,而互斥量的同名参数在该版本已被忽略并固定为 PRIO。

发送事件
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);

set 可以只包含一个事件位,也可以按位或组合多个事件位,但不能为 0。发送后内核会检查等待该对象的线程,唤醒所有已经满足其 OR/AND 条件的线程。rt_event_send() 可由线程或中断服务程序调用,但中断中只应发送通知,把耗时处理留给线程。

接收事件
rt_err_t rt_event_recv(rt_event_t event,
                       rt_uint32_t set,
                       rt_uint8_t option,
                       rt_int32_t timeout,
                       rt_uint32_t *recved);
参数含义
event要等待的事件集句柄
set线程关注的事件位掩码,不能为 0
optionOR 或 AND 二选一,并可额外按位或 CLEAR
timeout等待时间,单位是 OS tick
recved输出实际匹配到的位;不关心时可传 RT_NULL

条件已满足时立即返回 RT_EOK;条件不满足且 timeout == RT_WAITING_NO 时立即返回 -RT_ETIMEOUT;传入正数则最多等待相应 tick;RT_WAITING_FOREVER 表示永久等待。接收操作可能挂起当前线程,所以应在线程上下文使用,不能在中断服务程序中调用。

1.3 事件集应用示例

1.3.1 事件集应用示例的代码讲解

课程直接采用 RT-Thread 官方事件集示例,再按当前内核的宏命名和本工程习惯做适配。完整最终代码已经放在文首,整体分成四部分:

  1. 定义事件 3、事件 5,以及一个静态事件控制块。
  2. 为两个静态线程分别准备线程控制块和 1024 字节栈。
  3. thread1 接收事件,先演示 OR,再演示 AND;thread2 依次发送 3、5、3。
  4. event_sample() 初始化对象和线程,MSH_CMD_EXPORT 把它注册成 shell 命令。
1.3.2 RT-Thread项目的新建

课程创建 08EVENT 工程。配套配置显示:RT-Thread 版本为 5.0.2,芯片为 STM32F103ZE,GCC 工具链,ST-LINK 下载器,控制台为 UART1(PA9/PA10),系统 tick 频率为 1000 Hz,并已启用 RT_USING_EVENT、FinSH 和 MSH。

本节没有新增外部硬件或电路。事件集是内核中的软件对象;UART1 仅用于 MSH 命令输入和示例文本输出。

1.3.3 线程的创建与主函数

本例没有在 main() 中动态创建线程,而是采用静态线程接口:

rt_thread_init(&thread1, "thread1", thread1_recv_event, RT_NULL,
               &thread1_stack[0], sizeof(thread1_stack), 8, 5);

静态线程需要调用者自己提供两块存储:

  • struct rt_thread thread1:线程控制块,保存调度状态、优先级、栈指针等管理信息。
  • char thread1_stack[1024]:线程实际使用的栈空间。

thread1 的优先级为 THREAD_PRIORITY - 1 = 8,thread2 为 9,因此接收线程优先级更高。event_sample() 先启动 thread1,它因尚无事件而阻塞;随后启动 thread2,由它发送事件唤醒 thread1。

main() 只保留延时循环,且把 Hello RT-Thread! 日志注释掉,目的是不让周期日志干扰终端观察。event_sample() 并不会因为文件被编译就自动执行,它需要通过 MSH 命令显式调用。

1.3.4 事件集的使用例程

MSH 是 RT-Thread 的命令行 shell,属于 FinSH 组件;不是“fish”。下面的宏在链接时登记命令信息:

MSH_CMD_EXPORT(event_sample, event sample);

烧录并进入 msh > 提示符后,可输入:

event_sample

也可以先输入 eve 再按 Tab 补全。执行命令时,MSH 调用同名 C 函数;函数初始化事件对象与两个线程后返回,两个工作线程由调度器独立运行。

1.3.5 事件集的使用场合

事件集尤其适合以下组合条件:

  • 任一数据源就绪即可处理:等待 RX_READY | TIMEOUT | STOP,使用 OR。
  • 多项初始化都完成后再启动:等待 SENSOR_OK | NET_OK | CLOCK_OK,使用 AND。
  • 一个中断事件唤醒多个关注它的线程,或多个发送方共同设置一个接收线程关注的不同位。

选择 IPC 机制时要看语义:

需求更合适的机制
只关心某条件是否发生、还要组合多个条件事件集
必须累计发生次数计数信号量
需要传输一个数值邮箱
需要传输带内容的消息消息队列
保护共享资源,只允许一个线程进入互斥量
1.3.6 例题1: 事件集应用示例的代码添加与编译

课程在 applications 下新建 C 源文件,最终工程文件名为 event_test.c,然后把官方示例代码复制进去。.c 是源文件,.h 是声明和宏等通常使用的头文件;本例需要的是会参与编译的 C 源文件,而不是额外新建头文件。

示例通过 #include <rtthread.h> 获得线程、事件集和命令导出声明。编译通过只代表语法、声明和链接满足要求,仍需结合串口输出验证 OR、AND、CLEAR 和线程时序是否符合预期。

1.3.7 例题1的错误排查

课程从较旧的官方文档复制示例时,栈数组前使用的是旧宏 ALIGN(RT_ALIGN_SIZE);在 RT-Thread 5.0.2 工程中应使用当前头文件实际提供的 rt_align(RT_ALIGN_SIZE)。旧宏无法识别后,编译器会连带报告后面的栈变量或线程初始化出错,不能只根据最后一条报错就认定“静态线程变量没定义”。

排查这类问题应从第一条编译错误开始,并对照当前工程版本的头文件和更新记录:

  1. 找到第一个无法识别的标识符,而不是只修后续连锁错误。
  2. 在当前 rtdef.h 中搜索对应宏或接口。
  3. 确认课程/网页示例的 RT-Thread 版本是否与工程一致。
  4. 修改为当前版本真实存在的名称后重新完整编译。

1.4 内核API更新日志

RT-Thread 不同大版本会整理内部宏、数据类型和接口命名。课程遇到的是编译器属性宏从旧式大写命名向当前小写 rt_ 命名迁移的兼容问题,例如本例最终使用:

rt_align(RT_ALIGN_SIZE)

在本工程的 GCC 配置下,rt_align(n) 最终展开为编译器的 __attribute__((aligned(n)));RT-Thread 同时为 ARMCC、IAR、MSVC 等工具链提供不同展开方式,从而让业务代码使用统一宏。应以当前工程随附头文件为准,不能看到旧教程中的 ALIGN、RT_WEAK 等名称就机械照抄,也不应把所有大写标识符一律改成小写——例如 RT_ALIGN_SIZE、RT_EOK 仍是有效的公共宏。

1.5 字节对⻬

1.5.1 结构体对⻬的原因

对齐表示对象起始地址满足某个倍数关系。例如 4 字节对齐要求地址能被 4 整除。编译器按照目标 ABI 和成员类型的对齐要求,在结构体成员之间或末尾插入必要的填充,以便 CPU 正确、高效地访问数据。

但“结构体中的每个成员都占 4 字节”是错误的。以本工程常见 ABI 下的结构为例:

struct example
{
    char c; // c 自身仍只占 1 字节
    int  n; // n 通常需要从 4 字节边界开始
};

典型布局是 c 位于偏移 0,随后填充 3 字节,n 位于偏移 4;填充是为了让后续成员地址满足对齐,而不是把 char 的数据类型改成 4 字节。实际布局还受编译器、ABI、成员顺序和打包选项影响,应以 sizeof、offsetof 或编译器文档核对。

1.5.2 32位单片机与数据总线

STM32F103ZE 使用 32 位 Arm Cortex-M3 内核,通用寄存器和自然机器字长为 32 位。可以把四个连续字节看作一个 32 位字,但“32 位 MCU”不等于所有数据都固定用 32 根外部连线、每次必然传 32 位:指令可以进行 8、16、32 位访问,不同存储器和外设还经过各自的总线与接口。

对齐的重要性在于,一个自然对齐的 32 位对象位于合适边界,处理器和总线通常更容易按预期访问;未对齐对象可能需要拆分访问、降低效率,某些指令、区域或配置下还可能产生异常。因此内核会为线程栈、控制块等底层对象规定对齐要求。

1.5.3 字节对⻬的示例

以 4 字节边界为例:

对齐的 32 位对象:起始地址 0x20000000,占用 0x20000000~0x20000003
未对齐的 32 位对象:起始地址 0x20000001,占用 0x20000001~0x20000004

第二种布局跨过了两个相邻的 4 字节边界,硬件可能需要额外处理。课程用“2 秒和 3 秒”作示意并不是实际时间公式;访问耗时取决于处理器、指令、总线、存储区域、缓存和等待状态,不能仅按字节块数换算成秒。

1.5.4 代码中的字节对⻬操作

本工程配置:

#define RT_ALIGN_SIZE 4

所以:

rt_align(RT_ALIGN_SIZE)
static char thread1_stack[1024];

表示要求 thread1_stack 的起始地址至少按 4 字节对齐。数组仍由 1024 个 1 字节的 char 组成,总大小仍是 1024 字节;并不是每个数组元素各占 4 字节。对线程栈显式对齐可以满足 CPU 端口和 RT-Thread 上下文切换代码对栈地址的要求。

1.5.5 例题1: 字节对⻬的代码编译与运行

课程把两个线程栈前的旧 ALIGN 都改为 rt_align 后,示例通过编译并烧录。与此同时,main.c 中的周期日志被注释,以保持终端清晰;事件示例逻辑本身仍位于独立的 event_test.c,无需搬进 main()。

能被编译和链接的原因是 event_test.c 属于工程的应用源文件;真正开始运行示例则依赖 MSH_CMD_EXPORT 注册的命令,而不是系统在复位后自动调用 event_sample()。

1.5.6 例题2: 字节对⻬的代码调用

在 MSH 中执行 event_sample 后即可观察输出。该课程示例是一次性演示:两个线程执行完便返回,但静态事件对象没有调用 rt_event_detach(),代码也没有设计“重复初始化同一对象”的生命周期。因此不要把“每次必须复位”误解成 MSH 的通用限制;只是这份示例没有提供安全的重复运行和清理流程,复位能把所有静态对象恢复到初始状态。

1.6 事件集控制块

1.6.1 例题1: 事件控制块代码解析

事件控制块在 RT-Thread 5.0.2 中可概括为:

struct rt_event
{
    struct rt_ipc_object parent;
    rt_uint32_t set;
};

parent 保存内核对象信息和等待线程链表,set 保存 32 个事件位。本例的初始化顺序为:

  1. rt_event_init(&event, "event", RT_IPC_FLAG_PRIO) 把静态事件对象初始化为全 0。
  2. 初始化并启动高优先级的接收线程 thread1,它会在第一次 recv 处等待。
  3. 初始化并启动发送线程 thread2,由它设置事件位并唤醒接收线程。
1.6.2 事件接收与发送函数解析

第一次接收使用:

EVENT_FLAG3 | EVENT_FLAG5
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR

含义是事件 3、5 中任意一个出现即可返回,并清除本次对应事件位。第二次把 OR 改成 AND,要求两位都已经置 1 才返回。函数名是 rt_event_recv(),不是原稿中的 rt_event_receive。

发送端只需要指定事件对象和要置位的掩码:

rt_event_send(&event, EVENT_FLAG3);

本例没有检查三次 rt_event_send() 的返回值,是课堂示例的简化。产品代码应检查返回状态,尤其要避免传入 0 或在对象生命周期结束后继续发送。

1.6.3 事件标志与事件值解析

C 的位编号从 0 开始:

宏二进制示意十六进制人类从右数第几位
1 << 30000 10000x08第 4 位
1 << 50010 00000x20第 6 位
两者按位或0010 10000x28第 4、6 位

因此原稿中的 00010100 并不等于 0x28;正确的低 8 位表示是 0010 1000。

1.6.4 例题2: 事件接收与发送演示

典型输出顺序为:

thread2: send event3
thread1: OR recv event 0x8
thread1: delay 1s to prepare the second event
thread2: send event5
thread2: send event3
thread2 leave.
thread1: AND recv event 0x28
thread1 leave.

详细状态变化如下:

时刻动作event.set 变化结果
初始初始化事件对象0x00thread1 等待 OR 条件
第一次发送发送 EVENT_FLAG30x00 → 0x08OR 满足,thread1 得到 0x08;CLEAR 后回到 0x00
接收线程延时thread1 延时 1 秒暂无变化thread2 可继续发送
第二次发送发送 EVENT_FLAG50x00 → 0x20尚未同时具备 3、5
第三次发送再发 EVENT_FLAG30x20 → 0x28两个事件位都保留下来
第二次接收thread1 延时结束后执行 AND 接收读到 0x28,CLEAR 后为 0x00AND 立即成功

这里不是 thread1 正阻塞等待时由最后一次发送现场触发:由于它先延时 1 秒,thread2 在约 400 ms 内已经发送完事件 5 和事件 3,两个位先积累在事件对象中;thread1 醒来后检查到 AND 条件已经成立。

1.6.5 事件集的工作机制

事件集保存的是位状态而不是消息记录:

发送 EVENT_FLAG5:       0x00 OR 0x20 = 0x20
再发送 EVENT_FLAG3:     0x20 OR 0x08 = 0x28
AND 接收 0x08|0x20:    (0x28 & 0x28) == 0x28,条件成立
CLEAR:                  0x28 & ~0x28 = 0x00

如果在清除前再发送十次 EVENT_FLAG3,该位仍只有一个 1。事件集因此不能告诉接收方“事件 3 发生了十次”,只能告诉它“事件 3 至少发生过一次且尚未被清除”。

1.6.6 事件集相关语法解析

本例的 static 是 C 语言关键字,不存在“类加载”或“所有实例共享”等 Java/C++ 类语义。文件作用域下:

  • static struct rt_event event; 具有整个程序运行期的静态存储期,同时名称只在 event_test.c 内可见。
  • static char thread1_stack[1024]; 同样常驻内存,不会在函数返回后消失。
  • 这些未显式赋初值的静态存储对象在程序启动时先被清零,但仍必须调用 RT-Thread 的 init 接口建立合法内核对象状态。
1.6.7 C语言中static的作用

static 的含义取决于位置:

使用位置主要效果
文件作用域变量静态存储期,并限制为当前翻译单元内部链接
文件作用域函数函数名只在当前翻译单元可见
函数内局部变量作用域仍在该代码块内,但存储期延长到整个程序运行期,值会跨调用保留

“防止外部引用”适用于文件作用域的变量和函数;它有助于封装命名空间,但不能单独视为内存安全或访问权限机制。

1.6.8 代码细节与优化建议
  • rt_err_t 表达 RT-Thread API 的错误码语义,应优先使用与接口声明一致的类型,而不是仅因为当前平台上能用 int 就随意替换。
  • 用 THREAD_PRIORITY、THREAD_TIMESLICE 宏集中管理参数,能清楚显示两个线程的相对优先级。
  • event_sample() 把示例初始化封装成一个可由 MSH 调用的函数;所需头文件是源码中的 <rtthread.h>,不是原稿 OCR 出来的 “t4.h”。
  • 两个入口函数的 param 均未使用;rt_thread_init()、rt_thread_startup() 和 rt_event_send() 的返回值也未全部检查,这些是官方教学例程为了突出主流程所作的简化。
  • while (count++) 会让有符号 int 在长期运行后溢出,不宜作为产品代码中的永久循环条件;文首仍按课程最终源码原样保留。

核对依据: 最终源码、工程版本、对齐值和硬件配置取自课程配套 08EVENT 工程;事件对象结构、OR/AND/CLEAR 行为、等待时间和生命周期已与 RT-Thread 5.0.2 的 ipc.c、rtdef.h,以及官方线程间同步文档、事件集 API 文档和 FinSH 文档交叉核对。