同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的。
线程的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个 (或一类) 线程运行。
RT-Thread实现了三种线程间同步方式,信号量(semaphore)、互斥量(mutex)、和事件集(event)。
信号量
信号量可以实现多个同类资源的多线程互斥和同步。
特性:
- 不支持所有权,所有线程都可以操作。
- 递归访问可能造成死锁。
- 二值信号量类似互斥量,可能产生优先级反转。
使用场景:
- 线程同步。当持有信号量的线程完成它处理的工作时,释放这个信号量,可以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。这类场合也可以看成把信号量用于工作完成标志:持有信号量的线程完成它自己的工作,然后通知等待该信号量的线程继续下一部分工作。
- 锁。二值信号量,保护临界区资源。
- 中断与线程的同步。中断服务例程需要通知线程进行相应的数据处理。中断与线程间的互斥不能采用信号量(锁)的方式,而应采用开关中断的方式。
- 资源计数。例如生产者与消费者,生产者可以对信号量进行多次释放,而后消费者被调度到时能够一次处理多个信号量资源。
信号量控制块
信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
struct rt_ipc_object{
struct rt_object parent;
rt_list_t suspend_thread; /* 被挂起线程 */
};
struct rt_semaphore
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of semaphore. */
};
typedef struct rt_semaphore *rt_sem_t;
信号量初始化
sem->parent.parent.flag
信号量标志参数决定了当信号量不可用时,多个线程等待的排队方式。当选择 RT_IPC_FLAG_FIFO(先进先出)方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择 RT_IPC_FLAG_PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。
rt_err_t rt_sem_init(rt_sem_t sem, const char *name, rt_uint32_t value, rt_uint8_t flag)
{
rt_object_init(&(sem->parent.parent), RT_Object_Class_Semaphore, name);
rt_ipc_object_init(&(sem->parent));
sem->value = value;
sem->parent.parent.flag = flag;
return RT_EOK;
}
获取信号量
线程通过 rt_sem_take()
来获取信号量资源实例。当信号量值大于0时,线程获得信号量,同时该信号量值减1;当信号量值等于0时,表示资源不可用,线程通过time
参数立即返回或挂起。
释放信号量
释放信号量时,先判断是否有挂起线程。有则唤起一个,信号量值不变化;无则信号量值加一。
工程文件
线程2释放信号量,线程1获取。
互斥量
互斥量只能用于单个资源的互斥访问。一种特殊的二值信号量。
特性:
- 支持所有权,只有拥有该互斥量的线程才能操作。
- 支持递归访问。
- 通过优先级继承算法降低优先级反转问题产生的影响。
使用场景:
- 线程多次持有互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。
- 可能会由于多线程同步而造成优先级翻转的情况。
互斥量控制块
struct rt_mutex
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 互斥量的值 */
rt_uint8_t original_priority; /* 持有线程的原始优先级 */
rt_uint8_t hold; /* 持有线程的持有次数 */
struct rt_thread *owner; /* 当前拥有互斥量的线程 */
};
typedef struct rt_mutex* rt_mutex_t;
互斥量初始化
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag){
RT_ASSERT(mutex != RT_NULL);
rt_object_init(&mutex->parent.parent, RT_Object_Class_Mutex, name);
rt_ipc_object_init(&mutex->parent);
mutex->parent.parent.flag = flag;
mutex->value = 1;
mutex->original_priority = RT_THREAD_PRIORITY_MAX - 1;
mutex->hold = 0;
mutex->owner = RT_NULL;
return RT_EOK;
}
获取互斥量
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
线程获取了互斥量,那么线程就有了对该互斥量的所有权,即某一个时刻一个互斥量只能被一个线程持有。
如果互斥量已经被当前线程控制,则持有数mutex.hold
加一。如果互斥量被其他线程占有,则当前线程在互斥量上挂起等待,直到其他线程释放互斥量或等待时间超时,同时,如果当前线程的优先级高于互斥量拥有者的优先级,会发生优先级继承。
释放互斥量
rt_err_t rt_mutex_release(rt_mutex_t mutex)
只有互斥量的拥有者才能释放互斥量,持有数mutex.hold
减一。如果持有数变成零,则判断是否有线程挂起,有则唤起。
工程文件
thread1和thread2对全局变量number进行加1操作,都执行200000次,有mutex保护时,结果正确,开销大。
事件集
事件集用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件。
事件集控制块
struct rt_event
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint32_t set; /* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */
};
typedef struct rt_event *rt_event_t;
发送事件
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
发送事件时遍历事件集的挂起线程,判断是否有线程的事件激活要求与当前 event 对象事件标志值匹配,如果有,则唤醒该线程。
接收事件
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)
或者叫等待事件触发。系统首先根据 set 参数和接收选项 option 来判断它要接收的事件是否发生,如果已经发生,则根据参数 option 上是否设置有 RT_EVENT_FLAG_CLEAR 来决定是否重置事件的相应标志位,然后返回。如果没有发生,则把等待的 set 和 option 参数填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。