RT-Thread快速入门-线程间同步之信号量

简介: RT-Thread快速入门-线程间同步之信号量

线程同步是指多个线程通过某种特定的机制,来控制线程之间的先后执行顺序。

 

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_semaphorert_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,表示成功获取信号量。

目录
相关文章
|
2月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
114 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
1月前
|
Java 调度
Java 线程同步的四种方式,最全详解,建议收藏!
本文详细解析了Java线程同步的四种方式:synchronized关键字、ReentrantLock、原子变量和ThreadLocal,通过实例代码和对比分析,帮助你深入理解线程同步机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
Java 线程同步的四种方式,最全详解,建议收藏!
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
49 1
|
2月前
|
运维 API 计算机视觉
深度解密协程锁、信号量以及线程锁的实现原理
深度解密协程锁、信号量以及线程锁的实现原理
48 2
|
2月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
2月前
多线程通信和同步的方式有哪些?
【10月更文挑战第6天】
122 0
|
3月前
|
Java 数据中心 微服务
Java高级知识:线程池隔离与信号量隔离的实战应用
在Java并发编程中,线程池隔离与信号量隔离是两种常用的资源隔离技术,它们在提高系统稳定性、防止系统过载方面发挥着重要作用。
71 0
|
4月前
|
开发者 C# UED
WPF与多媒体:解锁音频视频播放新姿势——从界面设计到代码实践,全方位教你如何在WPF应用中集成流畅的多媒体功能
【8月更文挑战第31天】本文以随笔形式介绍了如何在WPF应用中集成音频和视频播放功能。通过使用MediaElement控件,开发者能轻松创建多媒体应用程序。文章详细展示了从创建WPF项目到设计UI及实现媒体控制逻辑的过程,并提供了完整的示例代码。此外,还介绍了如何添加进度条等额外功能以增强用户体验。希望本文能为WPF开发者提供实用的技术指导与灵感。
175 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
58 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
27 3