网站首页 > 技术文章 正文
>gzh【一起学嵌入式】
线程同步是指多个线程通过某种特定的机制,来控制线程之间的先后执行顺序。
RT-Thread 提供了几种线程同步的方式:信号量(semaphore)、 互斥量(mutex)、和事件集(event)。本篇文章主要介绍信号量相关的内容。
信号量的工作机制
信号量是一种可以用来解决线程间同步问题的内核对象,线程通过获取和释放信号量,来达到同步的目的。
每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值表示信号对象的实例数目或者资源数目;线程等待队列,由等待获取当前信号量的线程按照某种顺序排列而成。
当信号量值为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量资源。
信号量控制块
信号量控制块是 RT-Thread 用于管理信号量的一个数据结构,信号量控制块的结构体 struct rt_semaphore 定义如下,rt_sem_t 表示信号量的句柄,即指向信号量控制块的指针。
struct rt_semaphore
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 信号量的值 */
rt_uint16_t reserved; /* 保留域 */
};
/* rt_sem_t 为指向 rt_semaphore 结构体的指针类型 */
typedef struct rt_semaphore *rt_sem_t;
struct rt_semaphore 从 rt_ipc_object 派生而来,由 IPC 容器管理,信号量的最大值为 65535。
结构体struct rt_ipc_object parent 定义如下:
struct rt_object
{
char name[RT_NAME_MAX]; /* 内核对象名称 */
rt_uint8_t type; /* 内核对象类型 */
rt_uint8_t flag; /* 内核对象的参数 */
#ifdef RT_USING_MODULE
void *module_id; /* 应用程序模块 ID */
#endif
rt_list_t list; /* 内核对象管理链表 */
};
struct rt_ipc_object
{
struct rt_object parent; /* 继承自 rt_object */
rt_list_t suspend_thread; /* 挂起的线程链表 */
};
信号量控制块中含有信号量相关的重要参数,在信号量各种状态之间起到纽带的作用。
接下来看看如何对一个信号量进行操作。
管理信号量
RT-Thread 提供了一系列的函数接口,用于对信号量进行操作。包括:
- 创建/初始化信号量
- 获取信号量
- 释放信号量
- 删除/脱离信号量
常用的信号量操作为:创建信号量、获取信号量、释放信号量。下面重点介绍这三种操作。
1. 创建信号量
RT-Thread 创建信号量两种方式:动态创建和静态初始化。
跟其他内核对象类似,动态创建是由内核负责分配信号量控制块,然后对其进行基本的初始化工作。静态方式创建,是由用户负责定义一个信号量控制块结构体变量,然后调用初始化函数对其进行初始化工作。
动态创建信号量的函数接口如下:
rt_sem_t rt_sem_create(const char *name,
rt_uint32_t value,
rt_uint8_t flag)
当调用这个函数时,系统将先从对象管理器中分配一个 semaphore 对象,并初始化这个对象,然后初始化父类 IPC 对象以及与 semaphore 相关的部分。
该函数的各个参数解释如下:
参数 | 描述 |
name | 信号量名称 |
value | 信号量的初始值 |
flag | 创建信号量的标志 |
信号量创建成功,则返回信号量控制块的指针。创建失败,则返回 RT_NULL。
参数 flag 的作用是,当信号量不可用时,多个线程等待的排队方式。这个参数取值有两种:
- RT_IPC_FLAG_FIFO,先进先出方式。等待信号量的线程按照先进先出的方式排队,先进入的线程将先获得等待的信号量。
- RT_IPC_FLAG_PRIO,优先级等待方式。等待信号量的线程按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。
静态方式创建信号量,需要先定义一个信号量控制块结构 struct rt_semaphore 类型的变量,然后使用如下函数对其进行初始化:
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
这个函数参数,除了 sem,其他参数跟动态创建信号量函数 rt_sem_create() 的参数相同。
参数 sem 为信号量控制块的指针,指向用户定义的 struct rt_semaphore 结构变量的地址。
rt_sem_init() 函数的主要作用是,对 sem 指向的信号量控制块进行初始化操作。
该函数的返回值为 RT_EOK。
2. 获取信号量
线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1。如果信号量的值为零,说明当前信号量资源不可用,线程会获取失败。
RT-Thread 中获取信号量的函数如下:
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time)
参数 sem 表示信号量控制块指针(信号量的句柄)。
参数 time 表示线程等待获取信号量的时间,单位是系统时钟节拍。
调用此函数获取信号量时,如果信号量的值为零,线程将根据 time 参数的情况会有不同的动作:
- 参数值为零,则函数会直接返回。
- 参数值不为零,则会等待设定的时间。
- 参数值为最大时钟节拍数,则会永久等待,直到其他线程或中断释放该信号量。
如果在参数 time 指定的时间内没有获取到信号量,线程将超时返回,返回值为 -RT_ETIMEOUT。
rt_sem_take() 函数返回 RT_EOK,表示成功获得信号量。返回 -RT_ERROR, 表示其他错误。
线程获取信号量不可以用时,且等待时间 time不为零,
3. 释放信号量
释放信号量的系统函数如下:
rt_err_t rt_sem_release(rt_sem_t sem)
参数 sem 表示信号量控制块指针(信号量的句柄)。
释放信号量操作,根据具体情况,会有两种结果:
- 如果有线程等待获取这个信号量时,释放信号量将唤醒等待队列中的第一个线程,由它获取信号量,信号量的值仍然为零。
- 如果没有线程等待获取信号量,则信号量的值将会加 1。
实战演练
绝知此事要躬行。
通过具体的实例,来看看如何使用 RT-Thread 的信号量操作函数。动态创建一个信号量,创建两个线程,一个线程释放信号量,一个线程获取信号量后,执行后续的动作。
#include <rtthread.h>
#define THREAD_PRIORITY 25
#define THREAD_TIMESLICE 5
/* 指向信号量的指针 */
static rt_sem_t dynamic_sem = RT_NULL;
/* 线程1 入口函数 */
static void rt_thread1_entry(void *parameter)
{
static rt_uint8_t count = 0;
while(1)
{
if(count <= 100)
{
count++;
}
else
{
return;
}
/* count每计数 10 次, 就释放一次信号量 */
if(0 == (count % 10))
{
rt_kprintf("thread1 release a dynamic semaphore.\n");
rt_sem_release(dynamic_sem);
}
/* 延迟一会儿 */
rt_thread_delay(10);
}
}
/* 线程2 入口函数 */
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 (result != RT_EOK)
{
rt_kprintf("thread2 take a dynamic semaphore, failed.\n");
rt_sem_delete(dynamic_sem);
return;
}
else
{
number++;
rt_kprintf("thread2 take a dynamic semaphore. number = %d\n" ,number);
}
rt_thread_delay(10);
}
}
int main(void)
{
/* 线程控制块指针 */
rt_thread_t thread1 = RT_NULL;
rt_thread_t thread2 = RT_NULL;
/* 创建一个动态信号量,初始值是 0 */
dynamic_sem = rt_sem_create("dsem", 0, RT_IPC_FLAG_FIFO);
if (dynamic_sem == RT_NULL)
{
rt_kprintf("create dynamic semaphore failed.\n");
return -1;
}
else
{
rt_kprintf("create done. dynamic semaphore value = 0.\n");
}
/* 动态创建线程1 */
thread1 = rt_thread_create("thread1", rt_thread1_entry, RT_NULL,
1024, THREAD_PRIORITY, THREAD_TIMESLICE);
if(thread1 != RT_NULL)
{
/* 启动线程 */
rt_thread_startup(thread1);
}
/* 动态创建线程2 */
thread2 = rt_thread_create("thread2", rt_thread2_entry, RT_NULL,
1024, THREAD_PRIORITY-1, THREAD_TIMESLICE);
if(thread2 != RT_NULL)
{
/* 启动线程 */
rt_thread_startup(thread2);
}
return 0;
}
线程 1 在 count 计数为 10 的倍数时,释放一个信号量,线程 2 在接收到信号量后,对 number 进行加 1 操作。程序运行结果如下所示:
信号量的几种应用
我们先来看看线程的应用场景。线程可以用来当作资源锁、资源计数、线程间同步、中断与线程同步等。
1. 线程同步
使用信号量进行两个线程之间的同步,信号量的值初始化成 0,表示具备 0 个信号量资源实例;而尝试获得该信号量的线程,将直接在这个信号量上进行等待。
当持有信号量的线程完成它处理的工作时,释放这个信号量,可以把等待在这个信号量上的线程唤醒,让它执行下一部分工作。
这类场合也可以看成把信号量用于工作完成标志:持有信号量的线程完成它自己的工作,然后通知等待该信号量的线程继续下一部分工作。
2. 中断与线程同步
信号量可以用于中断与线程间的同步。例如,一个中断触发后,中断服务程序通知线程进行相应的数据处理。
此时,可以设置信号量的初始值为 0,线程在获取这个信号量时,由于信号量资源不足,线程会挂起直到这个信号量被释放。
当中断触发时,完成某些操作后,释放信号量来唤醒挂起线程,去进行后续的处理。
3. 锁(二值信号量)
信号量在当作锁来使用时,通常将信号量资源个数初始化为 1,表示默认只有一个资源可用。由于信号量的值始终在 1 和 0 之间变化,所以这类信号量也称为二值信号量。
当某个线程访问共享资源时,获得这个信号量。其他线程想要访问这个资源会由于获取不到资源而挂起。这是因为此时这个信号量的值为 0,其他线程获取不到。
当获取信号量的线程处理完毕,释放信号量后,会唤醒挂起队列中的第一个线程而获得资源的访问权限。
4. 资源计数
信号量可以认为是一个递增或递减的计数器,用于记录共享资源可以用的个数。线程访问共享资源时,信号量递减;结束访问后,信号量递增。
需要注意的是信号量的值非负。
其他函数接口介绍
除了上述常用的信号量操作函数,RT-Thread 还提供了其他管理函数,在此简单介绍一下,可以作为了解。
1. 删除信号量
由动态方式创建的信号量,可以用如下函数进行删除:
rt_err_t rt_sem_delete(rt_sem_t sem)
调用这个函数时,系统会删除信号量。如果有线程正在等待该信号量,则会先唤醒这些线程,然后再释放信号量占用的内存资源。
2. 脱离信号量
脱离信号量就是,让信号量对象从内核对象管理器中脱离。适用于通过静态方式初始化的信号量。脱离信号量的函数接口如下:
rt_err_t rt_sem_detach(rt_sem_t sem)
调用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号从内核对象管理器中脱离。
3. 无等待获取信号量
上面介绍的获取信号量函数 rt_sem_take() 有个等待时间参数,RT-Thread 提供了一种无等待方式获取信号量的函数接口,不用设置等待超时,函数原型如下:
rt_err_t rt_sem_trytake(rt_sem_t sem)
调用此函数获取信号量时,若线程申请的信号量资源不可用,它不会等待该信号量,而是直接返回错误码 -RT_ETIMEOUT。
如果函数返回 RT_EOK,表示成功获取信号量。
猜你喜欢
- 2024-10-29 RT-Thread快速入门-互斥量 互斥方案用什么指标
- 2024-10-29 Datenlord |内存顺序问题(二) dataloader 内存
- 2024-10-29 操作系统概论:第二章 进程管理 简述操作系统进程管理,并举例说明
- 2024-10-29 Java多线程操作系统(生产者、消费者问题)
- 2024-10-29 高可用架构-容错机制 容错技术可以提高系统的可靠性
- 2024-10-29 计算机操作系统笔记第二章进程管理中篇
- 2024-10-29 六大进程通信机制总结 进程通信有哪几种基本类型?
- 2024-10-29 Java系统过载保护机制之信号量的控制
- 2024-10-29 铂金04:通风报信-为何说信号量是线程间的同步良器
- 2024-10-29 记一次阿里面试题:都有哪些进程间通信方式?麻烦你不要再背了
你 发表评论:
欢迎- 最近发表
-
- 在 Spring Boot 项目中使用 activiti
- 开箱即用-activiti流程引擎(active 流程引擎)
- 在springBoot项目中整合使用activiti
- activiti中的网关是干什么的?(activiti包含网关)
- SpringBoot集成工作流Activiti(完整源码和配套文档)
- Activiti工作流介绍及使用(activiti工作流会签)
- SpringBoot集成工作流Activiti(实际项目演示)
- activiti工作流引擎(activiti工作流引擎怎么用)
- 工作流Activiti初体验及在数据库中生成的表
- Activiti工作流浅析(activiti6.0工作流引擎深度解析)
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)