目录
线程间同步
多个执行单元(线程、中断)同时执行临界区(多个执行单元会同时执行到的代码区域),操作临界资源,会导致竟态产生,为了解决这种竟态问题,RT-Thread OS提供了如下几种同步互斥机制:
- 信号量
- 互斥量
- 事件集
同步的概念是:多个执行单元执行临界区的时候按照顺序的方式执行
互斥的概念:互斥强调的是排他性,比如有两个线程A和B。当线程A执行的时候,线程B不能执行;线程B执行的时候,线程A不能执行。只有当一个线程执行完之后,另一个线程才能执行。
信号量
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为 5,则表示共有 5 个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
信号量结构体
当信号量被使用,value的值减1;当信号量被释放,value值加1
struct rt_semaphore
{
struct rt_ipc_object parent; /**< inherit from ipc_object 继承自ipc_object类*/
rt_uint16_t value; /**< value of semaphore. */
rt_uint16_t reserved; /**< reservedfield 预留*/
};typedef struct
rt_semaphore *rt_sem_t;
信号量的使用和管理
对一个信号量的操作包含:创建 /初始化信号量、获取信号量、释放信号量、删除 / 脱离信号量。
创建完信号量之后,需要找到临界区,在临界区之前获取信号量,在临界区之后要释放信号量。
动态创建信号量
参数1:信号量名字
参数2:信号量资源个数
参数3:#define RT_IPC_FLAG_FIFO 0x00 //按照线程先进先出获取信号量资源
#define RT_IPC_FLAG_PRIO 0x01 //按照线程优先级获取信号量资源
返回值为信号量结构体指针
/**
* This function will create a semaphore from system resource
*
* @param name the name of semaphore
* @param value the initial value of semaphore
* @param flag the flag of semaphore
*
* @return the created semaphore, RT_NULL on error happen
*
* @see rt_sem_init
*/
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)
系统不再使用信号量时,可通过删除信号量以释放系统资源,适用于动态创建的信号量。
调用这个函数时,系统将删除这个信号量。如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是-RT_ERROR),然后再释放信号量的内存资源。
/**
* This function will delete a semaphore objectand release the memory
*
* @param sem the semaphore object
*
* @return the error code
*
* @see rt_sem_detach
*/
rt_err_t rt_sem_delete(rt_sem_t sem)
实例
semaphore为信号量的名字,V为信号量的当前资源数,suspend thread为挂载在信号量上的线程数。但发现信号量名字少了最后面的“1”
注意名字设置的时候,必须要小于等于8个字符
静态创建信号量
初始化和脱离信号量
/**
* This function will initialize a semaphoreand put it under control of
* resource management.
*
* @param sem the semaphore object
* @param name the name of semaphore
* @param value the initial value of semaphore
* @param flag the flag of semaphore
*
* @return the operation status, RT_EOK onsuccessful
*/
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
脱离信号量就是让信号量对象从内核对象管理器中脱离,适用于静态初始化的信号量,
使用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中脱离。原来挂起在信号量上的等待线程将获得 - RT_ERROR 的返回值。
/**
* This function will detach a semaphore from resource management
*
* @param sem the semaphore object
*
* @return the operation status, RT_EOK on successful
*
* @see rt_sem_delete
*/
rt_err_t rt_sem_detach(rt_sem_t sem)
获取信号量
线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据time参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数time指定的时间内依然得不到信号量,线程将超时返回,返回值是 - RT_ETIMEOUT。
比如说现在有线程1和线程2两个线程同时都能执行临界区的代码,在临界区存在一个临界资源变量i。此时线程1和线程2是存在竞态关系的,我们可以通过信号量来阻止竞态的发生。
我们将信号量的value设置为1,在进入临界区之前,我们先获取信号量,在出临界区的时候再释放信号量。那么不管是线程1还是线程2,谁先执行临界区,都会先获取信号量,然后信号量value减1,变成0;此时线程2发现信号量value为0,就会选择休眠。直到线程1释放掉信号量,value值加1,就会唤醒线程2。那么线程2就会获取到信号量,从而操作临界区。
信号量的互斥操作
不管是谁先进入临界区操作临界区的资源,其它的线程就没办法来操作;只有等到操作完临界区的线程释放资源,其它的线程才能进行操作;每一个时刻只有一个线程可以操作临界区。
获取信号量函数
第一个参数为信号量的结构体指针
第二个参数为time,我们通过time可以知道如果线程获取不到信号量的时候是处于什么样的状态(如果为RT_WAITING_FOREVER就进入休眠状态一直等待信号量;RT_WAITING_NO为非阻塞的模式,如果线程获取不到信号量就直接返回,不会休眠;再然后其它的time值就是自己设置的等待时间:注意时间要大于0)
释放信号量
释放信号量可以唤醒挂起在该信号量上的线程
/**
* This function will release a semaphore, ifthere are threads suspended on
* semaphore, it will be waked up.
*
* @param sem the semaphore object
*
* @return the error code
*/
rt_err_t rt_sem_release(rt_sem_t sem)
信号量同步实例
(1)首先创建一个全局变量flags
(2)然后创建两个线程,让它们都能够操作到flags(我们将线程的优先级设置为相同优先级,这样才可能会导致竞态产生)
(3)此时我们想实现线程1和线程2的同步操作,即先执行线程1然后执行线程2,顺序执行
原理:比如说我们现在有两个信号量,信号量1初始化value为1,信号量2初始化value为0。线程1进入临界空间以后首先获取信号量1(信号量value大于1,获取成功),等线程1执行完之后信号量1会自动减1,然后释放信号量2,信号量2加1;此时线程2可以获取到信号量2,开始执行,执行完之后信号量2自动减1,然后释放信号量1,信号量1加1,然后线程1又开始执行了。按照顺序依次执行。
(4)创建两个信号量sem1和sem2
(5)完善线程执行函数:线程1先休眠8秒,线程2执行完一次再休眠2秒
(6)main函数中启动两个线程
(7)实验现象
烧写程序上电后,发现两个线程都处于挂起状态
因为线程1处于8秒的延时状态,而线程2此时无法获取到信号量
运行完效果:两个线程按顺序交替操作,flags的值在0-1跳转
互斥量(互斥锁)
互斥量体现的是排他性,也是解决多线程同时操作临界区临界资源导致的竟态的一种方法。(类似于特殊的信号量——二值信号量)
区别:信号量可由不同线程释放,互斥量只能由同一线程进行释放。
互斥量的使用和管理
互斥量的操作包含:创建 / 初始化互斥量、获取互斥量(相对于上锁)、释放互斥量(相对于解锁)、删除 / 脱离互斥量
动态创建互斥量
rt_mutex_create函数用于创建互斥量
参数1:信号量名字
参数2:#define RT_IPC_FLAG_FIFO 0x00 //按照线程先进先出获取信号量资源
#define RT_IPC_FLAG_PRIO 0x01 //按照线程优先级获取信号量资源
返回值为结构体指针变量rt_mutex_t
#ifdef RT_USING_HEAP
/**
* This function will create a mutex from system resource
*
* @param name the name of mutex
* @param flag the flag of mutex
*
* @return the created mutex, RT_NULL on error happen
*
* @see rt_mutex_init
*/
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag)
不再使用互斥量时,通过删除互斥量以释放系统资源,适用于动态创建的互斥量
当删除一个互斥量时,所有等待此互斥量的线程都将被唤醒,等待线程获得的返回值是
- RT_ERROR
/**
* This function will delete a mutex object andrelease the memory
*
* @param mutex the mutex object
*
* @return the error code
*
* @see rt_mutex_detach
*/
rt_err_t rt_mutex_delete(rt_mutex_t mutex)
动态创建过程
静态创建互斥量
初始化和脱离互斥量
注意传入的第一个参数是结构体变量的地址
/**
* This function will initialize a mutex andput it under control of resource
* management.
*
* @param mutex the mutex object
* @param name the name of mutex
* @param flag the flag of mutex
*
* @return the operation status, RT_EOK onsuccessful
*/
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag)
使用该函数接口后,内核先唤醒所有挂在该互斥量上的线程(线程的返回值是
-RT_ERROR) ,然后系统将该互斥量从内核对象管理器中脱离。
/**
* This function will detach a mutex fromresource management
*
* @param mutex the mutex object
*
* @return the operation status, RT_EOK onsuccessful
*
* @see rt_mutex_delete
*/
rt_err_t rt_mutex_detach(rt_mutex_t mutex)
静态创建过程
一般创建好互斥锁,是不会去删除或者分离的,如果要删除则分别使用delete和detach
获取互斥量
/**
* This function will take a mutex, if themutex is unavailable, the
* thread shall wait for a specified time.
*
* @param mutex the mutex object
* @param time the waiting time
*
* @return the error code
*/
rt_err_t rt_mutex_take(rt_mutex_tmutex, rt_int32_t time)
释放互斥量
/**
* This function will release a mutex, if thereare threads suspended on mutex,
* it will be waked up.
*
* @param mutex the mutex object
*
* @return the error code
*/
rt_err_t rt_mutex_release(rt_mutex_tmutex)
互斥锁实例
(1)首先创建两个临界资源flag1和flag2
(2)创建一个互斥锁
(3)创建两个线程
(4)在如下的情况下我们可以发现运行时,每次打印出来的flag1和flag2的值是不相等的
如果我们想要实现每次打印的两个值相等,则要确保线程1执行完成一次循环之后再执行线程2(即线程不会因为休眠挂起而被另一个线程操作临界区资源)。我们将线程1的while(1)设为临界区,进行上锁
(5)为相应临界区上锁和解锁
(6)结果现象
从输出的实验结果可以得知如果有两个线程使用同一把锁,则线程B必须等线程A执行完上锁和解锁及临界区的操作,才可以执行上锁操作,并继续往下执行,否则上锁失败,线程B就会进入挂起状态等待线程A解锁方可唤醒
事件集
事件集也是线程间同步的机制之一,一个事件集可以包含多个事件,利用事件集可以完成一对多,多对多的线程间同步。
一个线程和多个事件的关系可设置为:
其中任意一个事件唤醒 线程,或几个事件都到达后唤醒线程,多个事件集合可以用一个32bit无符号整型变量来表示,变量的每一位代表一个事件,线程通过"逻辑与"或"逻辑或"将一个或多个事件关联起来,形成事件组合。
RT-Thread 定义的事件集有以下特点:
- 事件只与线程相关,事件间相互独立
- 事件仅用于同步,不提供数据传输功能
- 事件无排队性,即多次向线程发送同一事件(如果线程还未来得及读走),其效果等同于只发送一次
动态创建事件集
创建和删除
/*
* event structure
*/
struct rt_event
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint32_t set; /**< event set */
};
typedef struct rt_event *rt_event_t;
/**
* This function will create an event objectfrom system resource
*
* @param name the name of event
* @param flag the flag of eventRT_IPC_FLAG_FIFO RT_IPC_FLAG_PRIO
*
* @return the created event, RT_NULL on errorhappen
*/
rt_event_t rt_event_create(const char*name, rt_uint8_t flag)
/**
* This function will delete an event objectand release the memory
*
* @param event the event object
*
* @return the error code
*/
rt_err_t rt_event_delete(rt_event_tevent)
静态创建事件集
初始化和脱离
/**
* This function will initialize an event andput it under control of resource
* management.
*
* @param event the event object
* @param name the name of event
* @param flag the flag of event
*
* @return the operation status, RT_EOK onsuccessful
*/
rt_err_t rt_event_init(rt_event_tevent, const char *name, rt_uint8_t flag)
/**
* This function will detach an event objectfrom resource management
*
* @param event the event object
*
* @return the operation status, RT_EOK onsuccessful
*/
rt_err_t rt_event_detach(rt_event_tevent)
事件集创建过程
(与前面类似,这里以动态为例)
事件集实现线程同步
事件集有多个事件,能够完成多个事件的同步。之前在完成线程同步的时候用的是信号量,但用到的是多个信号量才能完成线程的同步。而现在我们可以通过一个事件集来完成线程的同步。
事件集用的集合的类型是无符号32bit整形,事件的占位是一个事件占用一个位。所以一个集合里面支持32个事件。
发送事件
该函数给事件集对象发送一个事件,如果存在线程在事件集对象上挂起,它将会被唤醒
第一个参数为事件的结构体指针(事件集对象)
第二个参数为具体的事件值
/**
* This function will send an event to the event object, if there are threads
* suspended on event object, it will be wakedup.
*
* @param event the event object
* @param set the event set
*
* @return the error code
*/
rt_err_t rt_event_send(rt_event_tevent, rt_uint32_t set)
接收事件
该函数的功能是从事件集对象里面接收一个事件,如果事件是不可用的,线程将进入休眠(挂起状态)
第二个参数为指定具体接收是哪个事件
第三个参数为接收的操作,设置与和或的关系,还有是否设置在接收完某个事件后对位进行清除
第四个参数设置等待时间,与前面相同
第五个参数用于接收事件的值,如果不关心就不设置
/**
* This function will receive an event from event object, if the event is
* unavailable, the thread shall wait for aspecified time.
*
* @param event the fast event object
* @param set the interested event set
* @param option the receive option, eitherRT_EVENT_FLAG_AND or
* RT_EVENT_FLAG_OR should be set. RT_EVENT_FLAG_CLEAR
* @param timeout the waiting time RT_WAITING_FOREVER RT_WAITING_NO
* @param recved the received event, if youdon't care, RT_NULL can be set.
*
* @return the error code
*/
rt_err_trt_event_recv(rt_event_t event,
rt_uint32_t set,
rt_uint8_t option,
rt_int32_t timeout,
rt_uint32_t *recved)
实现原理
我们要用事件集实现三个线程的同步
如果采用信号量的话,我们需要三个信号。而采用事件集,我们只要使用三个事件位即可。
以事件2为例,当调度器调度到线程2,此时发现线程2没有接收到事件2,就无法往下执行,它就会挂起,从而线程3也无法接收到事件3。因此我们可以手动在main函数中手动发送一个事件,从而触发一个线程可以正常执行。
我们再使用事件集的时候,还可以用到or和and的关系。and:比如可以设置事件1和事件2同时满足的,线程1才可以执行;or:比如设置事件1或事件2有一个满足,线程1就可以执行
实现过程
(1)创建事件集
(3)创建三个线程
(4)发送、接收事件使用
在main中手动发送
(5)实验效果:每秒钟按照1、2、3的顺序执行
事件集实现进程同步源码
#include <rtthread.h>
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include <rtdbg.h>
rt_event_t event1;
rt_thread_t th1,th2,th3;
#define EVENT_FLAGS_1 (0x01<<0)
#define EVENT_FLAGS_2 (0x01<<1)
#define EVENT_FLAGS_3 (0x01<<2)
void th1_entry(void *parameter)
{
while(1){
rt_event_recv(event1,EVNET_FLAGS_1,RT_EVENT_FLAG_CLEAR | RT_EVENT_FLAG_AND,\
RT_WAITING_FOREVER,NULL);
rt_kprintf("th1_entry...\n");
rt_event_send(event1,EVENT_FLAGS_2);
rt_thread_mdelay(1000);
}
}
void th2_entry(void *parameter)
{
while(1){
rt_event_recv(event1,EVNET_FLAGS_2,RT_EVENT_FLAG_CLEAR | RT_EVENT_FLAG_AND,\
RT_WAITING_FOREVER,NULL);
rt_kprintf("th2_entry...\n");
rt_event_send(event1,EVENT_FLAGS_3);
rt_thread_mdelay(1000);
}
}
void th3_entry(void *parameter)
{
while(1){
rt_event_recv(event1,EVNET_FLAGS_3,RT_EVENT_FLAG_CLEAR | RT_EVENT_FLAG_AND,\
RT_WAITING_FOREVER,NULL);
rt_kprintf("th3_entry...\n");
rt_event_send(event1,EVENT_FLAGS_1);
rt_thread_mdelay(1000);
}
}
int main(void)
{
event1 = rt_event_create("set1", RT_IPC_FLAG_FIFO);
if(event1 == RT_NULL)
{
LOG_E("rt_event_create failed...\n");
return -ENOMEM;
}
LOG_D("rt_event_create succeed...\n");
th1 = rt_thread_create("th1",th1_entry,RT_NULL,512,10,5);
if(th1 == RT_NULL){
LOG_E("th1 rt_thread_create failed...\n");
return -ENOMEM;
}
th2 = rt_thread_create("th2",th2_entry,RT_NULL,512,10,5);
if(th2 == RT_NULL){
LOG_E("th2 rt_thread_create failed...\n");
return -ENOMEM;
}
th3 = rt_thread_create("th3",th3_entry,RT_NULL,512,10,5);
if(th3 == RT_NULL){
LOG_E("th3 rt_thread_create failed...\n");
return -ENOMEM;
}
rt_thread_startup(th1);
rt_thread_startup(th3);
rt_thread_startup(th2);
rt_event_send(event1,EVENT_FLAGS_1);
return RT_EOK;
}