线程同步是指多个线程通过某种特定的机制,来控制线程之间的先后执行顺序。
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
,表示成功获取信号量。