一、互斥量的基本概念
互斥量一般用于临界资源的保护。
互斥量又称互斥型信号量,是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性,用于实现对临界资源的独占式处理。任意时刻互斥量的状态只有两种:开锁或闭锁。当互斥量被线程持有时,该互斥量处于闭锁状态,这个线程获得互斥量的所有权。当该线程释放这个互斥量时,该互斥量处于开锁状态,线程失去该互斥量的所有权。当一个线程持有互斥量时,其他线程将不能再对该互斥量进行开锁或持有。持有该互斥量的线程也能够再次获得这个锁而不被挂起,这就是递归访问,这个特性与一般的二值信号量有很大的不同,在信号量中,由于已经不存在可用的信号量,线程递归获取信号量时会发生主动挂起(最终形成死锁)。
使用信号量会导致的另一个潜在问题是线程优先级翻转问题。所谓优先级翻转,即当一个高优先级线程试图通过信号量机制访问共享资源时,如果该信号量已被一低优先级线程持有,而这个低优先级线程在运行过程中可能又被其它一些中等优先级的线程抢占,因此造成高优先级线程被许多具有较低优先级的线程阻塞,实时性难以得到保证。如下图所示:有优先级为 A、B 和 C 的三个线程,优先级 A> B > C。线程 A,B 处于挂起状态,等待某一事件触发,线程 C 正在运行,此时线程 C 开始使用某一共享资源 M。在使用过程中,线程 A 等待的事件到来,线程 A 转为就绪态,因为它比线程 C 优先级高,所以立即执行。但是当线程 A 要使用共享资源 M 时,由于其正在被线程 C 使用,因此线程 A 被挂起切换到线程 C 运行。如果此时线程 B 等待的事件到来,则线程 B 转为就绪态。由于线程 B 的优先级比线程 C 高,因此线程 B 开始运行,直到其运行完毕,线程 C 才开始运行。只有当线程 C 释放共享资源 M 后,线程 A 才得以执行。在这种情况下,优先级发生了翻转:线程 B 先于线程 A 运行。这样便不能保证高优先级线程的响应时间。
如果想要用于实现同步(线程之间或者线程与中断之间),二值信号量或许是更好的选择,虽然互斥量也可以用于线程与线程、线程与中断的同步,但是互斥量更多的是用于保护资源的互锁。用于互锁的互斥量可以充当保护资源的令牌。当一个线程希望访问某个资源时,它必须先获取令牌。当线程使用完资源后,必须还回令牌,以便其它线程可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多线程的访问井然有序。当线程获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个线程才能获取到信号量从而可用使用被保护的资源。但是信号量会导致的另一个潜在问题,那就是线程优先级翻转(具体会在下文讲解)。而 RT-Thread 提供的互斥量
通过优先级继承算法,可用降低优先级翻转问题产生的影响,所以,用于临界资源的保护一般建议使用互斥量
二、互斥量的优先级继承机制
在 RT-Thread 操作系统中,互斥量可以解决优先级翻转问题,实现的是优先级继承算法。优先级继承是通过在线程 A 尝试获取共享资源而被挂起的期间内,将线程 C 的优先级提升到线程 A 的优先级别(将低优先级提升到高优先级),从而解决优先级翻转引起的问题。这样能够防止 C(间接地防止 A)被 B 抢占,如下图所示。优先级继承是指,提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。
在这里插入图片描述
三、互斥量的应用场景
互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量处于开锁的状态,而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于:
- 线程可能会多次获取互斥量的情况下。这样可以避免同一线程多次递归持有而造
成死锁的问题; - 可能会引起优先级翻转的情况;
多线程环境下往往存在多个线程竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。另外,互斥量可以降低信号量存在的优先级翻转问题带来的影响。
比如有两个线程需要对串口进行发送数据,其硬件资源只有一个,那么两个线程肯定不能同时发送,不然导致数据错误,那么,就可以用互斥量对串口资源进行保护,当一个线程正在使用串口的时候,另一个线程则无法使用串口,等到线程使用串口完毕之后,另外一个线程才能获得串口的使用权。
另外需要注意的是互斥量不能在中断服务函数中使用!互斥量不能在中断服务函数中使用!互斥量不能在中断服务函数中使用!。
四、互斥量使用注意事项
使用互斥量时候需要注意几点:
- 两个线程不能对同时持有同一个互斥量。如果某线程对已被持有的互斥量进行获取,则该线程会被挂起,直到持有该互斥量的线程将互斥量释放成功,其他线程才能申请这个互斥量。
- 互斥量不能在中断服务程序中使用。
- RT-Thread 作为实时操作系统需要保证线程调度的实时性,尽量避免线程的长时间阻塞,因此在获得互斥量之后,应该尽快释放互斥量。
- 持有互斥量的过程中,不得再调用 **rt_thread_control()**等函数接口更改持有互斥量线程的优先级
五、互斥量和信号量的区别
- 互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源
- 互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。
- 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
六、互斥量实验
互斥量同步实验是在 RT-Thread 中创建了两个线程,一个是申请互斥量线程,一个是释放互斥量线程,两个线程独立运行,申请互斥量线程是一直在等待互斥量线程的释放互斥量,其等待时间是 RT_WAITING_FOREVER,一直在等待,等到获取到互斥量之后,进行处理完它又马上释放互斥量。
释放互斥量线程模拟占用互斥量,延时的时间接收线程无法获得互斥量,等到线程使用互斥量完毕,然后进行互斥量的释放,接收线程获得互斥量,然后形成两个线程间的同步,若是线程正常同步,则在串口打印出信息。
#include "mutex.h"
#include "board.h"
/*
******************************************************************
* 变量
******************************************************************
*/
/* 定义线程控制块 */
static rt_thread_t receive_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
/* 定义互斥量控制块 */
static rt_mutex_t test_mux = RT_NULL;
/************************* 全局变量声明 ****************************/
/*
* 当我们在写应用程序的时候,可能需要用到一些全局变量。
*/
//uint8_t ucValue [ 2 ] = { 0x00, 0x00 };
/*
*************************************************************************
* 函数声明
*************************************************************************
*/
static void receive_thread_entry(void* parameter);
static void send_thread_entry(void* parameter);
int Mutex_Init(void)
{
/*
* 开发板硬件初始化,RTT系统初始化已经在main函数之前完成,
* 即在component.c文件中的rtthread_startup()函数中完成了。
* 所以在main函数中,只需要创建线程和启动线程即可。
*/
rt_kprintf("这是一个RTT互斥量同步实验!\n");
rt_kprintf("同步成功则输出Successful,反之输出Fail\n");
/* 创建一个互斥量 */
test_mux = rt_mutex_create("test_mux",RT_IPC_FLAG_PRIO);
if (test_mux != RT_NULL)
rt_kprintf("互斥量创建成功!\n\n");
receive_thread = /* 线程控制块指针 */
rt_thread_create( "receive", /* 线程名字 */
receive_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
512, /* 线程栈大小 */
3, /* 线程的优先级 */
20); /* 线程时间片 */
/* 启动线程,开启调度 */
if (receive_thread != RT_NULL)
rt_thread_startup(receive_thread);
else
return -1;
send_thread = /* 线程控制块指针 */
rt_thread_create( "send", /* 线程名字 */
send_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数参数 */
512, /* 线程栈大小 */
2, /* 线程的优先级 */
20); /* 线程时间片 */
/* 启动线程,开启调度 */
if (send_thread != RT_NULL)
rt_thread_startup(send_thread);
else
return -1;
}
/*
*************************************************************************
* 线程定义
*************************************************************************
*/
static void receive_thread_entry(void* parameter)
{
/* 任务都是一个无限循环,不能返回 */
while(1)
{
rt_mutex_take(test_mux, /* 获取互斥量 */
RT_WAITING_FOREVER); /* 等待时间:一直等 */
if ( ucValue [ 0 ] == ucValue [ 1 ] )
{
rt_kprintf ( "Successful\n" );
}
else
{
rt_kprintf ( "Fail\n" );
}
rt_mutex_release( test_mux ); //释放互斥量
rt_thread_delay ( 1000 ); //每1s读一次
}
}
static void send_thread_entry(void* parameter)
{
/* 任务都是一个无限循环,不能返回 */
while (1)
{
rt_mutex_take(test_mux, /* 获取互斥量 */
RT_WAITING_FOREVER); /* 等待时间:一直等 */
ucValue [ 0 ] ++;
rt_thread_delay ( 100 ); /* 延时100ms */
ucValue [ 1 ] ++;
rt_mutex_release( test_mux ); //释放互斥号量
rt_thread_yield(); //放弃剩余时间片,进行一次任务切换
}
}
下载编译:rtthread互斥量实验工程
参考文献:
1、[野火®]《RT-Thread 内核实现与应用开发实战—基于STM32》
2、《RT-THREAD 编程指南》