互斥量
互斥量 (Mutex),又称互斥锁,是一种特殊的二值信号量,用于实现对共享资源的互斥访问。互斥量可以理解为只有一个车位的停车场:当一辆车进入时,停车场大门被锁上,其他车辆需要在外面等待。当里面的车驶出时,停车场大门打开,下一辆车才能进入。
互斥量工作机制
互斥量与信号量的主要区别在于:
- 所有权: 拥有互斥量的线程拥有该互斥量的所有权。互斥量只能由持有线程释放,而信号量可以由任何线程释放。
- 递归访问: 互斥量支持线程的递归访问,即同一线程可以多次获取同一个互斥量而不会被阻塞。
- 优先级翻转保护: 互斥量具有防止线程优先级翻转的机制,而信号量则没有。
互斥量只有两种状态:开锁 (解锁) 或闭锁 (加锁)。当一个线程持有互斥量时,互斥量处于闭锁状态,并且该线程拥有该互斥量的所有权。当该线程释放互斥量时,互斥量变为开锁状态,该线程失去互斥量的所有权。当一个线程持有互斥量时,其他线程无法获取该互斥量。持有互斥量的线程可以多次获取同一个互斥量而不会被挂起,这是互斥量与二值信号量的一个重要区别:信号量在没有可用实例时,线程递归持有会导致主动挂起,最终形成死锁。
互斥量与优先级翻转问题
使用信号量可能会导致线程优先级翻转问题。优先级翻转是指,当一个高优先级线程试图访问共享资源时,如果该资源被一个低优先级线程占用,而该低优先级线程在运行时又可能被其他中等优先级的线程抢占,导致高优先级线程被许多低优先级线程阻塞,无法及时获得执行。
例如,假设有优先级分别为 A > B > C 的三个线程。线程 A 和 B 处于挂起状态,等待事件触发。线程 C 正在运行并开始使用共享资源 M。当线程 A 等待的事件到达时,线程 A 转为就绪态,由于其优先级高于线程 C,因此立即执行。但是,当线程 A 试图访问共享资源 M 时,发现其正在被线程 C 使用,因此线程 A 被挂起,切换到线程 C 执行。如果此时线程 B 等待的事件到达,线程 B 转为就绪态。由于线程 B 的优先级高于线程 C,且线程 B 没有使用共享资源 M,因此线程 B 开始运行,直至运行完毕。只有当线程 C 释放共享资源 M 后,线程 A 才能继续执行。在这种情况下,优先级发生了翻转:线程 B 先于线程 A 执行。这导致高优先级线程的响应时间无法得到保证。
互斥量与优先级继承
RT-Thread 互斥量通过优先级继承协议解决优先级翻转问题。当线程 A 尝试获取已被线程 C 占用的互斥量时,线程 C 的优先级会被提升至线程 A 的优先级,从而避免了线程 C 被线程 B 抢占。这样可以防止 C(间接地防止 A)被 B 抢占。
优先级继承是指,提高持有资源的低优先级线程的优先级,使其与所有等待该资源线程中优先级最高的线程优先级相等,然后执行。当该低优先级线程释放资源时,优先级恢复为初始设置。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。
注意: 在获得互斥量后,请尽快释放互斥量。在持有互斥量的过程中,不得更改持有互斥量线程的优先级,否则可能引入无界优先级翻转的问题。
互斥量控制块
在 RT-Thread 中,互斥量控制块是一个用于管理互斥量的数据结构,用 struct rt_mutex
表示。 rt_mutex_t
是互斥量的句柄,在 C 语言中表示指向互斥量控制块的指针。互斥量控制块结构的详细定义如下:
struct rt_mutex
{
struct rt_ipc_object parent; /* 继承自 ipc_object 类 */
rt_uint16_t value; /* 互斥量的值 */
rt_uint8_t original_priority;/* 持有线程的原始优先级 */
rt_uint8_t hold; /* 持有线程的持有次数 */
struct rt_thread *owner; /* 当前拥有互斥量的线程 */
};
typedef struct rt_mutex *rt_mutex_t;
rt_mutex
对象继承自 rt_ipc_object
,并由 IPC 容器管理。
互斥量的管理方式
互斥量控制块中包含互斥相关的关键参数。对互斥量的操作包括:创建/初始化、获取、释放和删除/脱离。
创建和删除互斥量
创建互斥量**
可以使用 rt_mutex_create()
函数创建一个动态互斥量:
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag);
调用此函数时,系统将从对象管理器中分配一个 mutex
对象并初始化,然后初始化父类 IPC 对象以及与 mutex
相关的部分。flag
参数已废弃,内核统一按 RT_IPC_FLAG_PRIO
处理。 rt_mutex_create()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
name | 互斥量的名称。 |
flag | 此标志已作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO ,内核均按 RT_IPC_FLAG_PRIO 处理。 |
返回值 | 描述 |
互斥量句柄 | 创建成功。 |
RT_NULL | 创建失败。 |
删除互斥量
可以使用 rt_mutex_delete()
函数删除动态创建的互斥量,以释放系统资源:
rt_err_t rt_mutex_delete(rt_mutex_t mutex);
删除互斥量时,所有等待该互斥量的线程都会被唤醒,并返回错误码 -RT_ERROR
。然后,系统会将该互斥量从内核对象管理器链表中删除并释放其占用的内存空间。rt_mutex_delete()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
mutex | 互斥量句柄。 |
返回值 | 描述 |
RT_EOK | 删除成功。 |
初始化和脱离互斥量
初始化静态互斥量**
对于静态互斥量对象(即在编译时分配的互斥量),在使用前需要先进行初始化,可以使用 rt_mutex_init()
函数进行初始化:
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag);
初始化时,需要指定互斥量句柄(指向互斥量控制块的指针)、互斥量名称以及互斥量标志。flag
参数已废弃,内核统一按 RT_IPC_FLAG_PRIO
处理。 rt_mutex_init()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
mutex | 互斥量句柄,由用户提供,指向互斥量控制块内存地址。 |
name | 互斥量的名称。 |
flag | 此标志已作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO ,内核均按 RT_IPC_FLAG_PRIO 处理。 |
返回值 | 描述 |
RT_EOK | 初始化成功。 |
脱离互斥量
rt_mutex_detach()
函数用于将静态初始化的互斥量对象从内核对象管理器中脱离:
rt_err_t rt_mutex_detach(rt_mutex_t mutex);
调用此函数后,内核首先唤醒所有挂在该互斥量上的线程(线程的返回值为 -RT_ERROR
),然后系统将该互斥量从内核对象管理器中脱离。rt_mutex_detach()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
mutex | 互斥量句柄。 |
返回值 | 描述 |
RT_EOK | 脱离成功。 |
获取互斥量
获取互斥量**
可以使用 rt_mutex_take()
函数获取互斥量:
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time);
如果互斥量未被其他线程占用,则申请互斥量的线程成功获得互斥量。如果互斥量已经被当前线程占用,则互斥量的持有计数加 1,当前线程不会被挂起。如果互斥量被其他线程占用,则当前线程在该互斥量上挂起等待,直到其他线程释放互斥量或者等待时间超过指定的超时时间。rt_mutex_take()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
mutex | 互斥量句柄。 |
time | 指定等待的时间。 |
返回值 | 描述 |
RT_EOK | 成功获得互斥量。 |
-RT_ETIMEOUT | 超时。 |
-RT_ERROR | 获取失败。 |
无等待获取互斥量
rt_mutex_trytake()
函数用于无等待方式获取互斥量:
rt_err_t rt_mutex_trytake(rt_mutex_t mutex);
此函数与 rt_mutex_take(mutex, RT_WAITING_NO)
的作用相同,当互斥量资源不可用时,线程不会等待,而是直接返回 -RT_ETIMEOUT
。 rt_mutex_trytake()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
mutex | 互斥量句柄。 |
返回值 | 描述 |
RT_EOK | 成功获得互斥量。 |
-RT_ETIMEOUT | 获取失败。 |
释放互斥量
当线程完成互斥资源的访问后,应尽快释放互斥量。可以使用 rt_mutex_release()
函数释放互斥量:
rt_err_t rt_mutex_release(rt_mutex_t mutex);
只有拥有互斥量控制权的线程才能释放互斥量,每释放一次,互斥量的持有计数减 1。当持有计数为零时,互斥量变为可用,等待在该互斥量上的线程将被唤醒。如果线程的运行优先级被互斥量提升,则释放互斥量后,线程的优先级恢复为持有互斥量前的优先级。rt_mutex_release()
函数的参数和返回值说明如下:
参数 | 描述 |
---|---|
mutex | 互斥量句柄。 |
返回值 | 描述 |
RT_EOK | 成功。 |
互斥量应用示例
互斥量例程
以下是一个使用互斥量的应用示例,描述了一个生活中的常见场景:
场景: 银行自动取款机
描述: 银行自动取款机是一个共享资源,多个用户可能同时尝试使用它。为了防止多个用户同时操作取款机,导致数据混乱或错误,我们需要使用互斥量来保护取款机的访问。
代码实现:
#include <rtthread.h>
// 定义互斥量
rt_mutex_t atm_mutex = RT_NULL;
// 取款机结构体
typedef struct {
rt_uint32_t balance;
} ATM;
ATM atm;
// 用户线程
void user_thread(void *parameter)
{
rt_uint32_t amount = 100;
rt_kprintf("用户:%s要取款%d元\n", (char *)parameter, amount);
// 获取互斥量
rt_mutex_take(atm_mutex, RT_WAITING_FOREVER);
// 取款
if (atm.balance >= amount) {
atm.balance -= amount;
rt_kprintf("用户:%s取款成功,余额:%d元\n", (char *)parameter, atm.balance);
} else {
rt_kprintf("用户:%s取款失败,余额不足\n", (char *)parameter);
}
// 释放互斥量
rt_mutex_release(atm_mutex);
}
int main(void)
{
// 初始化取款机
atm.balance = 1000;
// 创建互斥量
atm_mutex = rt_mutex_create("atm_mutex", RT_IPC_FLAG_FIFO);
if (atm_mutex == RT_NULL) {
rt_kprintf("创建互斥量失败\n");
return -1;
}
// 创建用户线程
rt_thread_t user1_tid = rt_thread_create("user1", user_thread, "用户1", 1024, 10, 10);
rt_thread_t user2_tid = rt_thread_create("user2", user_thread, "用户2", 1024, 10, 10);
rt_thread_t user3_tid = rt_thread_create("user3", user_thread, "用户3", 1024, 10, 10);
if (user1_tid!= RT_NULL) {
rt_thread_startup(user1_tid);
}
if (user2_tid!= RT_NULL) {
rt_thread_startup(user2_tid);
}
if (user3_tid!= RT_NULL) {
rt_thread_startup(user3_tid);
}
return 0;
}
在这个示例中,我们使用互斥量atm_mutex
来保护取款机的访问。每个用户线程在取款前需要获取互斥量,取款后释放互斥量。这保证了只有一个用户可以同时访问取款机,防止数据混乱或错误。
实验现象:
防止优先级翻转特性例程
以下是一个防止优先级翻转特性的例程,描述了一个生活中的常见场景:
场景: 医院手术室
描述: 医院手术室是一个共享资源,多个医生可能同时需要使用它。为了防止低优先级的医生占用手术室,导致高优先级的医生无法及时进行手术,我们需要使用优先级继承协议来防止优先级翻转。
代码实现:
#include <rtthread.h>
// 定义互斥量
rt_mutex_t surgery_mutex = RT_NULL;
// 手术室结构体
typedef struct {
rt_uint8_t available;
} Surgery;
Surgery surgery;
// 医生线程
void doctor_thread(void *parameter)
{
rt_uint8_t priority = *(rt_uint8_t *)parameter;
rt_kprintf("医生:%d要进行手术\n", priority);
// 获取互斥量
rt_mutex_take(surgery_mutex, RT_WAITING_FOREVER);
// 进行手术
surgery.available = 0;
rt_kprintf("医生:%d正在进行手术\n", priority);
rt_thread_delay(RT_TICK_PER_SECOND * 2);
// 释放互斥量
surgery.available = 1;
rt_mutex_release(surgery_mutex);
}
int main(void)
{
// 初始化手术室
surgery.available = 1;
// 创建互斥量
surgery_mutex = rt_mutex_create("surgery_mutex", RT_IPC_FLAG_FIFO);
if (surgery_mutex == RT_NULL) {
rt_kprintf("创建互斥量失败\n");
return -1;
}
// 创建医生线程
rt_uint8_t priority1 = 1;
rt_uint8_t priority2 = 2;
rt_uint8_t priority3 = 3;
rt_thread_t doctor1_tid = rt_thread_create("doctor1", doctor_thread, &priority1, 1024, 10, 10);
rt_thread_t doctor2_tid = rt_thread_create("doctor2", doctor_thread, &priority2, 1024, 10, 10);
rt_thread_t doctor3_tid = rt_thread_create("doctor3", doctor_thread, &priority3, 1024, 10, 10);
if (doctor1_tid!= RT_NULL) {
rt_thread_startup(doctor1_tid);
}
if (doctor2_tid!= RT_NULL) {
rt_thread_startup(doctor2_tid);
}
if (doctor3_tid!= RT_NULL) {
rt_thread_startup(doctor3_tid);
}
return 0;
}
实验现象:
互斥量的使用场合
互斥量的使用场景比较单一,它是一种特殊的二值信号量,以锁的形式存在。互斥量在初始化时处于开锁状态,被线程持有后立即变为闭锁状态。互斥量更适合以下场景:
- 线程多次持有互斥量的情况,可以避免同一线程多次递归持有而造成死锁。
- 可能因多线程同步而导致优先级翻转的情况,互斥量可以通过优先级继承协议防止优先级翻转。