rt-thread 线程间同步方法详解
一、什么是线程间同步
线程同步是指多线程环境下,通过特定机制协调线程的执行顺序,避免数据竞争和资源冲突。当多个线程共享同一资源时,无序访问可能导致数据不一致或程序错误,同步机制确保线程有序、安全地操作共享资源。
线程同步的必要性
多线程并发执行时,若未进行同步控制,可能引发以下问题:
- 竞态条件(Race Condition):多个线程同时修改共享数据,导致结果依赖线程执行顺序。
- 数据不一致:线程读取到中间状态或未更新的数据。
- 死锁:线程互相等待对方释放资源,导致程序僵持。
线程同步的挑战
- 性能开销:锁机制可能导致线程阻塞,降低并发效率。
- 死锁风险:需避免循环等待、持有锁时请求其他锁等情况。
- 调试难度:多线程问题通常难以复现和定位。
在多线程实时系统中,一项工作的完成往往可以通过多个线程协调的方式共同来完成,那么多个线程之间如何 "默契" 协作才能使这项工作无差错执行?下面举个例子说明。
例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:
如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。
二、同步方式
1、信号量
以生活中的停车场为例来理解信号量的概念:
- 当停车场空的时候,停车场的管理员发现有很多空车位,此时会让外面的车陆续进入停车场获得停车位;
- 当停车场的车位满的时候,管理员发现已经没有空车位,将禁止外面的车进入停车场,车辆在外排队等候;
- 当停车场内有车离开时,管理员发现有空的车位让出,允许外面的车进入停车场;待空车位填满后,又禁止外部车辆进入。
在此例子中,管理员就相当于信号量,管理员手中空车位的个数就是信号量的值(非负数,动态变化);停车位相当于公共资源(临界区),车辆相当于线程。车辆通过获得管理员的允许取得停车位,就类似于线程通过获得信号量访问公共资源。
信号量工作机制
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
信号量工作示意图如下图所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
信号量的管理方式
信号量控制块中含有信号量相关的重要参数,在信号量各种状态间起到纽带的作用。信号量相关接口如下图所示,对一个信号量的操作包含:创建 / 初始化信号量、获取信号量、释放信号量、删除 / 脱离信号量。
信号量的创建与删除
动态创建信号量使用rt_sem_create
函数,需指定名称和初始值:
c
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag);
参数flag
通常设置为RT_IPC_FLAG_FIFO
(按队列顺序唤醒)或RT_IPC_FLAG_PRIO
(按优先级唤醒)。
静态信号量通过rt_sem_init
初始化:
c
rt_err_t rt_sem_init(rt_sem_t sem, const char *name, rt_uint32_t value, rt_uint8_t flag);
删除动态信号量用rt_sem_delete
,释放静态信号量用rt_sem_detach
。
信号量的获取与释放
获取信号量使用rt_sem_take
,可设置超时时间:
c
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t timeout);
timeout
为RT_WAITING_FOREVER
表示永久等待,RT_WAITING_NO
表示不等待。
无阻塞尝试获取信号量用rt_sem_trytake
:
c
rt_err_t rt_sem_trytake(rt_sem_t sem);
释放信号量用rt_sem_give
:
c
rt_err_t rt_sem_release(rt_sem_t sem);
信号量的典型应用场景
资源互斥访问
通过二进制信号量保护共享资源:
c
rt_sem_t sem_mutex = rt_sem_create("mutex", 1, RT_IPC_FLAG_FIFO);
/* 线程A */
rt_sem_take(sem_mutex, RT_WAITING_FOREVER);
/* 访问共享资源 */
rt_sem_release(sem_mutex);
/* 线程B */
rt_sem_take(sem_mutex, RT_WAITING_FOREVER);
/* 访问共享资源 */
rt_sem_release(sem_mutex);
线程同步
生产者-消费者模型中,用信号量通知数据就绪:
c
rt_sem_t sem_data = rt_sem_create("data", 0, RT_IPC_FLAG_FIFO);
/* 生产者线程 */
produce_data();
rt_sem_release(sem_data);
/* 消费者线程 */
rt_sem_release(sem_data, RT_WAITING_FOREVER);
consume_data();
应用示例
c
#include <rtthread.h>
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
/* 信号量退出标志 */
static rt_bool_t sem_flag = 0;
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
static void rt_thread1_entry(void *parameter)
{
static rt_uint8_t count = 0;
while (1)
{
if (count <= 100)
{
count++;
}
else
{
rt_kprintf("thread1 exiting...\n");
sem_flag = 1;
rt_sem_release(dynamic_sem);
count = 0;
return;
}
/* count 每计数 10 次,就释放一次信号量 */
if (0 == (count % 10))
{
rt_kprintf("t1 release a dynamic semaphore.\n");
rt_sem_release(dynamic_sem);
}
}
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
static void rt_thread2_entry(void *parameter)
{
static rt_err_t result;
static rt_uint8_t number = 0;
while (1)
{
/* 永久方式等待信号量,获取到信号量,则执行 number 自加的操作 */
result = rt_sem_take(dynamic_sem, RT_WAITING_FOREVER);
if (sem_flag && result == RT_EOK)
{
rt_kprintf("thread2 exiting...\n");
rt_sem_delete(dynamic_sem);
sem_flag = 0;
number = 0;
return;
}
else
{
number++;
rt_kprintf("t2 take a dynamic semaphore. number = %d\n", number);
}
}
}
/* 信号量示例的初始化 */
int semaphore_sample(void)
{
/* 创建一个动态信号量,初始值是 0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_PRIO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}
rt_thread_init(&thread1,
"thread1",
rt_thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
rt_thread2_entry,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(semaphore_sample, semaphore sample);
运行结果
c
\ | /
- RT - Thread Operating System
/ | \ 4.1.1 build Sep 2 2024 14:52:06
2006 - 2022 Copyright by RT-Thread team
msh >semaphore_sample
create done. dynamic semaphore value = 0.
msh >thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 1
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 2
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 3
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 4
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 5
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 6
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 7
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 8
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 9
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 10
thread1 exiting...
thread2 exiting...
msh >semaphore_sample
create done. dynamic semaphore value = 0.
msh >thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 1
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 2
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 3
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 4
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 5
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 6
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 7
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 8
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 9
thread1 release a dynamic semaphore.
thread2 take a dynamic semaphore. number = 10
thread1 exiting...
thread2 exiting...
信号量的注意事项
- 优先级反转问题:高优先级线程等待低优先级线程释放信号量时,可能被中优先级线程抢占。可通过优先级继承或屏蔽中断解决。
- 死锁风险:避免多个信号量嵌套获取时形成循环等待。
- 性能影响:频繁的信号量操作会增加系统开销,需合理设计临界区范围。
RT-Thread还提供rt_sem_control
函数修改信号量属性,如调整等待线程的唤醒顺序策略。实际开发中应根据场景选择信号量类型,并配合其他IPC机制(如互斥锁、事件集)使用。
2、互斥量
互斥量又叫相互排斥的信号量,是一种特殊的二值信号量。互斥量类似于只有一个车位的停车场:当有一辆车进入的时候,将停车场大门锁住,其他车辆在外面等候。当里面的车出来时,将停车场大门打开,下一辆车才可以进入。
互斥量工作机制
互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。
互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它时,互斥量处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起,如下图时所示。这个特性与一般的二值信号量有很大的不同:在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁)。
互斥量的特性
- 独占性:互斥量一次只能被一个线程持有,其他线程必须等待。
- 优先级继承:当高优先级线程等待低优先级线程释放互斥量时,低优先级线程会临时继承高优先级,以避免优先级反转。
- 递归锁:同一线程可以多次获取互斥量,但需要对应次数的释放操作。
互斥量的操作接口
RT-Thread 提供以下 API 用于操作互斥量:
c
// 创建互斥量
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag);
// 删除互斥量
rt_err_t rt_mutex_delete(rt_mutex_t mutex);
// 获取互斥量(阻塞)
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t timeout);
// 释放互斥量
rt_err_t rt_mutex_release(rt_mutex_t mutex);
互斥量的使用示例
以下是一个简单的互斥量使用示例,展示如何保护共享资源:
c
#include <rtthread.h>
static rt_mutex_t mutex;
static int shared_data = 0;
void thread1_entry(void *parameter)
{
while (1) {
rt_mutex_take(mutex, RT_WAITING_FOREVER);
shared_data++;
rt_kprintf("Thread1: shared_data = %d\n", shared_data);
rt_mutex_release(mutex);
rt_thread_mdelay(100);
}
}
void thread2_entry(void *parameter)
{
while (1) {
rt_mutex_take(mutex, RT_WAITING_FOREVER);
shared_data--;
rt_kprintf("Thread2: shared_data = %d\n", shared_data);
rt_mutex_release(mutex);
rt_thread_mdelay(100);
}
}
int mutex_sample(void)
{
mutex = rt_mutex_create("test_mutex", RT_IPC_FLAG_FIFO);
if (mutex == RT_NULL) {
rt_kprintf("create mutex failed.\n");
return -1;
}
rt_thread_t tid1 = rt_thread_create("thread1", thread1_entry, RT_NULL, 512, 20, 10);
rt_thread_t tid2 = rt_thread_create("thread2", thread2_entry, RT_NULL, 512, 20, 10);
if (tid1 != RT_NULL) rt_thread_startup(tid1);
if (tid2 != RT_NULL) rt_thread_startup(tid2);
return 0;
}
互斥量与信号量的区别
- 用途不同:互斥量用于保护共享资源,信号量用于线程间同步。
- 所有权:互斥量具有所有者(获取的线程),信号量没有所有者。
- 优先级继承:互斥量支持优先级继承,信号量不支持。
- 计数:互斥量只能是 0 或 1,信号量可以有多个计数。
注意事项
- 避免死锁:确保互斥量的获取和释放成对出现,避免嵌套获取互斥量导致死锁。
- 优先级设置:合理设置线程优先级,以充分发挥优先级继承机制的作用。
- 超时设置:在获取互斥量时设置合理的超时时间,避免线程长期阻塞。
RT-Thread 的互斥量机制为多线程环境下的资源共享提供了可靠保障,合理使用可以显著提升系统的稳定性和性能。
3、事件集
事件集也是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。下面以坐公交为例说明事件,在公交站等公交时可能有以下几种情况:
- P1 坐公交去某地,只有一种公交可以到达目的地,等到此公交即可出发。
- P1 坐公交去某地,有 3 种公交都可以到达目的地,等到其中任意一辆即可出发。
- P1 约另一人 P2 一起去某地,则 P1 必须要等到 "同伴 P2 到达公交站" 与"公交到达公交站"两个条件都满足后,才能出发。
这里,可以将 P1 去某地视为线程,将 "公交到达公交站"、"同伴 P2 到达公交站" 视为事件的发生,情况①是特定事件唤醒线程;情况②是任意单个事件唤醒线程;情况③是多个事件同时发生才唤醒线程。
事件集工作机制
事件集主要用于线程间的同步,与信号量不同,它的特点是可以实现一对多,多对多的同步。即一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。这种多个事件的集合可以用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 "逻辑与" 或"逻辑或"将一个或多个事件关联起来,形成事件组合。事件的 "逻辑或" 也称为是独立型同步,指的是线程与任何事件之一发生同步;事件 "逻辑与" 也称为是关联型同步,指的是线程与若干事件都发生同步。
RT-Thread 定义的事件集有以下特点:
- 事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件;
- 事件仅用于同步,不提供数据传输功能;
- 事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。
在 RT-Thread 中,每个线程都拥有一个事件信息标记,它有三个属性,分别是 RT_EVENT_FLAG_AND(逻辑与),RT_EVENT_FLAG_OR(逻辑或)以及 RT_EVENT_FLAG_CLEAR(清除标记)。当线程等待事件同步时,可以通过 32 个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。
事件集的管理方式
事件集控制块中含有与事件集相关的重要参数,在事件集功能的实现中起重要的作用。事件集相关接口如下图所示,对一个事件集的操作包含:创建 / 初始化事件集、发送事件、接收事件、删除 / 脱离事件集。
-
创建事件集
crt_event_t rt_event_create(const char *name, rt_uint8_t flag);
name
:事件集名称,用于调试。flag
:可选RT_IPC_FLAG_FIFO
(默认)或RT_IPC_FLAG_PRIO
(按优先级唤醒线程)。
-
删除事件集
crt_err_t rt_event_delete(rt_event_t event);
需确保没有线程正在等待该事件集。
-
发送事件
crt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
set
:事件标志位,如0x01
表示事件1,0x03
表示事件1和2。
-
接收事件
crt_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);
option
:RT_EVENT_FLAG_AND
(与逻辑)或RT_EVENT_FLAG_OR
(或逻辑)。timeout
:等待超时时间(RT_WAITING_FOREVER
为永久等待)。recved
:输出参数,返回实际接收到的事件标志位。
事件集使用示例
c
#include <rtthread.h>
#define THREAD_PRIORITY 9
#define THREAD_TIMESLICE 5
#define EVENT_FLAG3 (1 << 3)
#define EVENT_FLAG5 (1 << 5)
/* 事件控制块 */
static struct rt_event event;
ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程 1 入口函数 */
static void thread1_recv_event(void *param)
{
rt_uint32_t e;
/* 第一次接收事件,事件 3 或事件 5 任意一个可以触发线程 1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: OR recv event 0x%x\n", e);
}
rt_kprintf("thread1: delay 1s to prepare the second event\n");
rt_thread_mdelay(1000);
/* 第二次接收事件,事件 3 和事件 5 均发生时才可以触发线程 1,接收完后清除事件标志 */
if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5),
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER, &e) == RT_EOK)
{
rt_kprintf("thread1: AND recv event 0x%x\n", e);
}
/* 执行完该事件集后进行事件集的脱离,事件集重复初始化会导致再次运行时,出现重复初始化的问题 */
rt_event_detach(&event);
rt_kprintf("thread1 leave.\n");
}
ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_send_event(void *param)
{
rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_thread_mdelay(200);
rt_kprintf("thread2: send event5\n");
rt_event_send(&event, EVENT_FLAG5);
rt_thread_mdelay(200);
rt_kprintf("thread2: send event3\n");
rt_event_send(&event, EVENT_FLAG3);
rt_kprintf("thread2 leave.\n");
}
int event_sample(void)
{
rt_err_t result;
/* 初始化事件对象 */
result = rt_event_init(&event, "event", RT_IPC_FLAG_PRIO);
if (result != RT_EOK)
{
rt_kprintf("init event failed.\n");
return -1;
}
rt_thread_init(&thread1,
"thread1",
thread1_recv_event,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
THREAD_PRIORITY - 1, THREAD_TIMESLICE);
rt_thread_startup(&thread1);
rt_thread_init(&thread2,
"thread2",
thread2_send_event,
RT_NULL,
&thread2_stack[0],
sizeof(thread2_stack),
THREAD_PRIORITY, THREAD_TIMESLICE);
rt_thread_startup(&thread2);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(event_sample, event sample);
运行结果:
c
\ | /
- RT - Thread Operating System
/ | \ 4.1.1 build Sep 5 2024 15:53:21
2006 - 2022 Copyright by RT-Thread team
msh >event_sample
thread2: send event3
thread1: OR recv event 0x8
thread1: delay 1s to prepare the second event
msh >thread2: send event5
thread2: send event3
thread2 leave.
thread1: AND recv event 0x28
thread1 leave.
msh >event_sample
thread2: send event3
thread1: OR recv event 0x8
thread1: delay 1s to prepare the second event
msh >thread2: send event5
thread2: send event3
thread2 leave.
thread1: AND recv event 0x28
thread1 leave.
事件集注意事项
- 事件标志位:通常使用32位无符号整数,每位代表一个独立事件。
- 线程唤醒逻辑 :
RT_EVENT_FLAG_OR
在任一事件发生时唤醒线程;RT_EVENT_FLAG_AND
需所有事件均发生。 - 资源管理 :动态创建的事件集需手动删除,静态初始化可用
rt_event_init
。 - 优先级反转:高优先级线程可能因等待低优先级线程发送事件而被阻塞,需合理设计线程优先级。
三、对比
以下为 RT-Thread 中信号量、事件集、互斥量的对比表格:
信号量、事件集、互斥量对比
特性 | 信号量 | 事件集 | 互斥量 |
---|---|---|---|
用途 | 线程同步/资源计数 | 多事件触发同步 | 临界区资源独占访问 |
资源类型 | 计数器(二进制/计数) | 32位标志位(每位代表一个事件) | 所有权机制(带优先级继承) |
释放方式 | rt_sem_release() |
rt_event_send() |
rt_mutex_release() |
获取方式 | rt_sem_take() |
rt_event_recv() |
rt_mutex_take() |
阻塞超时 | 支持 | 支持 | 支持 |
优先级继承 | 不支持 | 不支持 | 支持 |
递归锁 | 不支持 | 不支持 | 支持(同一线程重复获取) |
适用场景 | 任务调度、资源分配 | 多条件触发(如按键+定时器) | 共享资源保护(如全局变量) |
关键区别说明
信号量
- 二进制信号量:计数器值为 0 或 1,用于简单同步。
- 计数信号量:计数器值可大于 1,表示剩余资源数量。
事件集
- 支持"或"触发(任一事件满足)和"与"触发(所有事件满足)。
- 事件标志位可手动清除或自动清除。
互斥量
- 避免优先级反转:持有互斥量的低优先级线程会临时继承高优先级。
- 必须由获取线程释放,不可跨线程操作。
