一、互斥量
(一)互斥量的定义
互斥量的状态只有两种,开锁或闭锁(两种状态值)。当有线程持有它后,互斥量将处于闭锁状态,由这个线程获得它的所有权。相反,当这个线程释放它时,将对互斥量进行开锁,失去它的所有权。
当一个线程持有互斥量时,其他线程将不能够对它进行开锁或持有它,持有该互斥量的线程也能够再次获得这个锁而不被挂起。
多线程环境下往往存在多个线程竞争同一临界资源的应用场景,互斥量可被用于对临界资源的保护从而实现独占式访问。
(二)互斥量的使用
1、互斥量控制块
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; /* 当前拥有互斥量的线程 */
};
/* rt_mutext_t 为指向互斥量结构体的指针类型 */
typedef struct rt_mutex* rt_mutex_t;
2、获取互斥量
rt_err_t rt_mutex_take (rt_mutex_t mutex, rt_int32_t time);
如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程控制,则该互斥量的持有计数加 1,当前线程也不会挂起等待。如果互斥量已经被其他线程占有,则当前线程在该互斥量上挂起等待,直到其他线程释放它或者等待时间超过指定的超时时间。
3、释放互斥量
rt_err_t rt_mutex_release(rt_mutex_t mutex);
当线程完成互斥资源的访问后,应尽快释放它占据的互斥量,使得其他线程能及时获取该互斥量。
使用该函数接口时,只有已经拥有互斥量控制权的线程才能释放它,当持有该互斥量的线程释放该互斥量,该互斥量的持有计数就减 1。当该互斥量的持有计数为零时,该互斥量变为可用(开锁状态),等待在该信号量上的线程将被唤醒。如果线程的运行优先级被互斥量提升,那么当互斥量被释放后,线程恢复为持有互斥量前的优先级。
(三)互斥量的使用场合
互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。在初始化的时候,互斥量永远都处于开锁的状态,而被线程持有的时候则立刻转为闭锁的状态。互斥量更适合于:
(1)线程多次持有互斥量的情况下。这样可以避免同一线程多次递归持有而造成死锁的问题。
(2)可能会由于多线程同步而造成优先级翻转的情况。
注:需要切记的是互斥量不能在中断服务例程中使用。
(四)与信号量的区别
1)一个互斥量在同一时刻只能被一个线程持有,而不是像信号量可以同时被多个线程持有(信号量值≥2)。
2)互斥量支持递归访问,而信号量递归持有会发生主动挂起,最终形成死锁;
3)互斥量不会造成线程优先级翻转,而信号量使用不当可能会造成优先级翻转;
4)互斥量只能由持有线程释放,而信号量则可以由任何线程释放。
注:互斥量是0开锁,1闭锁;而信号量是0闭锁,≥1是释放(开锁)。
三、互斥量相关
(一)优先级翻转
优先级翻转是当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证。
例如:有优先级为A、B和C三个任务,优先级A>B>C,任务A,B处于挂起状态,等待某一事件发生,任务C正在运行,此时任务C开始使用某一共享资源S。在使用中,任务A等待事件到来,任务A转为就绪态,因为它比任务C优先级高,所以立即执行。当任务A要使用共享资源S时,由于其正在被任务C使用,因此任务A被挂起,任务C开始运行。如果此时任务B等待事件到来,则任务B转为就绪态。由于任务B优先级比任务C高,因此任务B开始运行,直到其运行完毕,任务C才开始运行。直到任务C释放共享资源S后,任务A才得以执行。在这种情况下,优先级发生了翻转,任务B先于任务A运行。
(二)优先级继承算法
优先级继承算法可以很好的解决优先级翻转问题。
优先级继承是通过在线程 A 尝试获取共享资源而被挂起的期间内,将线程 C 的优先级提升到线程 A 的优先级别,从而解决优先级翻转引起的问题。这样能够防止 C(间接地防止 A)被 B 抢占,如下图所示。优先级继承是指,提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。
注:针对优先级继承算法,低优先级线程在获得互斥量后要尽快释放互斥量,并且在持有互斥量的过程中,不得再行更改持有互斥量线程的优先级。
(三)死锁
1、单一线程递归持有信号量造成的自死锁
某一个线程持有一信号量,并递归调用了该信号量,造成信号量的值为0后,线程停下无法运行(相当于该线程被自己锁住了)。
2、两个线程相互死锁
使用互斥锁的时候,两个线程分别开启了一把锁,造成两个线程相互死锁。
(四)递归锁
foo函数和bar函数都获取了同一个锁,而bar函数又会调用foo函数。如果MutexLock锁是个非递归锁,则这个程序会立即死锁。因此在为一段程序加锁时要格外小心,否则很容易因为这种调用关系而造成死锁。不要存在侥幸心理,觉得这种情况是很少出现的。当代码复杂到一定程度,被多个人维护,调用关系错综复杂时,程序中很容易犯这样的错误。庆幸的是,这种原因造成的死锁很容易被排除。
但是这并不意味着应该用递归锁去代替非递归锁。递归锁用起来固然简单,但往往会隐藏某些代码问题。比如调用函数和被调用函数以为自己拿到了锁,都在修改同一个对象,这时就很容易出现问题。因此在能使用非递归锁的情况下,应该尽量使用非递归锁,因为死锁相对来说,更容易通过调试发现。程序设计如果有问题,应该暴露的越早越好。
MutexLock mutex;
void foo()
{
mutex.lock();
// do something
mutex.unlock();
}
void bar()
{
mutex.lock();
// do something
foo();
mutex.unlock();
}