同步是指按预定的先后次序进行运行,线程同步是指多个线程通过特定的机制(如互斥量,事件对象,临界区)来控制线程之间的执行顺序,也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间将是无序的。
线程的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个 (或一类) 线程运行。
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 参数填入线程本身的结构中,然后把线程挂起在此事件上,直到其等待的事件满足条件或等待时间超过指定的超时时间。