线程同步_事件集
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 |
option | OR 或 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 官方事件集示例,再按当前内核的宏命名和本工程习惯做适配。完整最终代码已经放在文首,整体分成四部分:
- 定义事件 3、事件 5,以及一个静态事件控制块。
- 为两个静态线程分别准备线程控制块和 1024 字节栈。
thread1接收事件,先演示 OR,再演示 AND;thread2依次发送 3、5、3。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)。旧宏无法识别后,编译器会连带报告后面的栈变量或线程初始化出错,不能只根据最后一条报错就认定“静态线程变量没定义”。
排查这类问题应从第一条编译错误开始,并对照当前工程版本的头文件和更新记录:
- 找到第一个无法识别的标识符,而不是只修后续连锁错误。
- 在当前
rtdef.h中搜索对应宏或接口。 - 确认课程/网页示例的 RT-Thread 版本是否与工程一致。
- 修改为当前版本真实存在的名称后重新完整编译。
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 个事件位。本例的初始化顺序为:
rt_event_init(&event, "event", RT_IPC_FLAG_PRIO)把静态事件对象初始化为全 0。- 初始化并启动高优先级的接收线程
thread1,它会在第一次recv处等待。 - 初始化并启动发送线程
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 << 3 | 0000 1000 | 0x08 | 第 4 位 |
1 << 5 | 0010 0000 | 0x20 | 第 6 位 |
| 两者按位或 | 0010 1000 | 0x28 | 第 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 变化 | 结果 |
|---|---|---|---|
| 初始 | 初始化事件对象 | 0x00 | thread1 等待 OR 条件 |
| 第一次发送 | 发送 EVENT_FLAG3 | 0x00 → 0x08 | OR 满足,thread1 得到 0x08;CLEAR 后回到 0x00 |
| 接收线程延时 | thread1 延时 1 秒 | 暂无变化 | thread2 可继续发送 |
| 第二次发送 | 发送 EVENT_FLAG5 | 0x00 → 0x20 | 尚未同时具备 3、5 |
| 第三次发送 | 再发 EVENT_FLAG3 | 0x20 → 0x28 | 两个事件位都保留下来 |
| 第二次接收 | thread1 延时结束后执行 AND 接收 | 读到 0x28,CLEAR 后为 0x00 | AND 立即成功 |
这里不是 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 文档交叉核对。