【QT之QMutex QRecursiveMutex】互斥锁 递归锁

QMutex 互斥锁

QMutex的目的是保护对象、数据结构或代码段,以便一次只有一个线程可以访问它(这类似于Java synchronized关键字)。通常最好将互斥锁与QMutexLocker一起使用,因为这样可以很容易地确保一致执行锁定和解锁。
例如,假设有一种方法在两行上向用户打印消息:

int number = 6;
void method1()
{
    number *= 5;
    number /= 4;
}
void method2()
{
    number *= 3;
    number /= 2;
}

如果连续调用这两个方法,则会发生以下情况:

// method1()
number *= 5;        // number is now 30
number /= 4;        // number is now 7

// method2()
number *= 3;        // number is now 21
number /= 2;        // number is now 10

如果从两个线程同时调用这两个方法,则可能会产生以下序列:

// Thread 1 calls method1()
number *= 5;        // number is now 30
// Thread 2 calls method2().
// Most likely Thread 1 has been put to sleep by the operating
// system to allow Thread 2 to run.
number *= 3;        // number is now 90
number /= 2;        // number is now 45

// Thread 1 finishes executing.
number /= 4;        // number is now 11, instead of 10

如果我们添加互斥,我们应该得到我们想要的结果:

QMutex mutex;
int number = 6;

void method1()
{
    mutex.lock();
    number *= 5;
    number /= 4;
    mutex.unlock();
}

void method2()
{
    mutex.lock();
    number *= 3;
    number /= 2;
    mutex.unlock();
}

然后在任何给定的时间只有一个线程可以修改数字,结果是正确的。当然,这是一个微不足道的例子,但适用于任何其他需要按特定顺序发生的情况。
当您在一个线程中调用lock()时,其他试图在同一位置调用lock()的线程将阻塞,直到获得锁的线程调用unlock()为止。lock()的一个非阻塞替代方案是tryLock()。
QMutex经过优化,在非争夺情况下速度更快。如果互斥体上没有争夺,那么非递归QMutex将不会分配内存。它是在几乎没有开销的情况下构造和销毁的,这意味着将许多互斥对象作为其他类的一部分是可以的。

enum QMutex::RecursionMode
常量Value描述
QMutex::Recursive1在这种模式下,一个线程可以多次锁定同一个互斥体,并且在进行相应数量的unlock()调用之前,互斥体不会被解锁。
QMutex::NonRecursive0在这种模式下,线程只能锁定一次互斥对象。
QMutex::QMutex(QMutex::RecursionMode mode)
//构造一个新的互斥对象。互斥锁是在未锁定状态下创建的。
//如果模式是QMutex::Recursive,则线程可以多次锁定同一个互斥对象,
//并且在进行相应数量的unlock()调用之前,该互斥对象不会被解锁。
//否则,线程可能只锁定一次互斥对象。默认值为QMutex::NonRecursive。
//递归互斥量比非递归互斥量慢,占用的内存也更多。
bool QMutex::isRecursive() const
//如果这个互斥量为递归返回true.
void QMutex::lock()
//锁定互斥对象。如果另一个线程锁定了互斥锁,那么这个调用将被阻塞,直到该线程将其解锁。
//如果该互斥对象是递归互斥对象,则允许在同一线程的同一互斥对象上多次调用该函数。
//如果这个互斥对象是非递归互斥对象,那么当互斥对象被递归锁定时,这个函数将死锁定。
bool QMutex::try_lock()
//尝试锁定互斥对象。如果已获得锁,此函数将返回true;否则返回false。
//提供此功能是为了与标准库概念Lockable兼容。它相当于tryLock()。

bool QMutex::tryLock(int timeout = 0)
//尝试锁定互斥对象。如果已获得锁,此函数将返回true;否则返回false。
//如果另一个线程锁定了互斥锁,则此函数最多会等待超时毫秒,以便互斥锁可用。
template <typename Rep, typename Period> bool QMutex::try_lock_for(std::chrono::duration<Rep, Period> duration)
//尝试锁定互斥对象。如果已获得锁,此函数将返回true;否则返回false。
//如果另一个线程锁定了互斥锁,此函数将等待duration时间,以使互斥锁可用。
//注意:传递一个负的持续时间作为持续时间相当于调用try_lock()。此行为与tryLock()不同。
template <typename Clock, typename Duration> bool QMutex::try_lock_until(std::chrono::time_point<Clock, Duration> timePoint)
//尝试锁定互斥对象。如果已获得锁,此函数将返回true;否则返回false。
//如果另一个线程锁定了互斥锁,此函数将等待timepoint时间,以使互斥锁可用。
//注意:传递一个timepoint作为持续时间相当于调用try_lock()。此行为与tryLock()不同。
以上三个方法
//如果获得了锁,则必须使用unlock()解锁互斥锁,然后另一个线程才能成功锁定它。
//如果该互斥对象是递归互斥对象,则允许在同一线程的同一互斥对象上多次调用该函数。
//如果这个互斥锁是非递归互斥锁,那么当试图递归锁定互斥锁时,这个函数总是返回false。
void QMutex::unlock()
//解锁互斥锁。试图解锁与锁定互斥锁的线程不同的线程中的互斥锁会导致错误。
//解锁未锁定的互斥对象会导致未定义的行为。

什么是非递归和递归锁?

这里参考这篇博文

Mutex可以分为递归锁(recursive mutex)和非递归锁(non-recursive mutex)。可递归锁也可称为可重入锁(reentrant mutex),非递归锁又叫不可重入锁(non-reentrant mutex)。
二者唯一的区别是,同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。
Windows下的Mutex和Critical Section是可递归的。Linux下的pthread_mutex_t锁默认是非递归的。可以显示的设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁。

在大部分介绍如何使用互斥量的文章和书中,这两个概念常常被忽略或者轻描淡写,造成很多人压根就不知道这个概念。但是如果将这两种锁误用,很可能会造成程序的死锁。请看下面的程序。

MutexLock mutex;
void foo()
{
    mutex.lock();
    // do something
    mutex.unlock();
}

void bar()
{
    mutex.lock();
    // do something
    foo();
    mutex.unlock();    
}

foo函数和bar函数都获取了同一个锁,而bar函数又会调用foo函数。如果MutexLock锁是个非递归锁,则这个程序会立即死锁。因此在为一段程序加锁时要格外小心,否则很容易因为这种调用关系而造成死锁。

不要存在侥幸心理,觉得这种情况是很少出现的。当代码复杂到一定程度,被多个人维护,调用关系错综复杂时,程序中很容易犯这样的错误。庆幸的是,这种原因造成的死锁很容易被排除。

但是这并不意味着应该用递归锁去代替非递归锁。递归锁用起来固然简单,但往往会隐藏某些代码问题。比如调用函数和被调用函数以为自己拿到了锁,都在修改同一个对象,这时就很容易出现问题。因此在能使用非递归锁的情况下,应该尽量使用非递归锁,因为死锁相对来说,更容易通过调试发现。程序设计如果有问题,应该暴露的越早越好。

如何避免

   为了避免上述情况造成的死锁,AUPE v2一书在第12章提出了一种设计方法。即如果一个函数既有可能在已加锁的情况下使用,也有可能在未加锁的情况下使用,往往将这个函数拆成两个版本---加锁版本和不加锁版本(添加nolock后缀)。

例如将foo()函数拆成两个函数。

// 不加锁版本
void foo_nolock()
{
    // do something
}
// 加锁版本
void fun()
{
    mutex.lock();
    foo_nolock();
    mutex.unlock();
}

为了接口的将来的扩展性,可以将bar()函数用同样方法拆成bar_without_lock()函数和bar()函数。

在Douglas C. Schmidt(ACE框架的主要编写者)的“Strategized Locking, Thread-safe Interface, and Scoped Locking”论文中,提出了一个基于C++的线程安全接口模式(Thread-safe interface pattern),与AUPE的方法有异曲同工之妙。即在设计接口的时候,每个函数也被拆成两个函数,没有使用锁的函数是private或者protected类型,使用锁的的函数是public类型。接口如下:

class T
{
public:
    foo(); //加锁
    bar(); //加锁
private:
    foo_nolock();
    bar_nolock();
}

作为对外接口的public函数只能调用无锁的私有变量函数,而不能互相调用。在函数具体实现上,这两种方法基本是一样的。
上面讲的两种方法在通常情况下是没问题的,可以有效的避免死锁。但是有些复杂的回调情况下,则必须使用递归锁。比如foo函数调用了外部库的函数,而外部库的函数又回调了bar()函数,此时必须使用递归锁,否则仍然会死锁。

【QT之QRecursiveMutex】 递归锁

描述

QRecursiveMutex类是一个互斥对象,类似于QMutex,它与API兼容。它与QMutex的不同之处在于,它多次接受来自同一线程的lock()调用。QMutex在这种情况下会deadlock(死锁)。
QRecursiveMutex的构建和操作成本要高得多,因此请尽可能使用普通的QMutex。然而,有时一个公共函数调用另一个公共功能,它们都需要锁定同一个互斥对象。在这种情况下,您有两个选项:
1、将需要互斥保护的代码分解为私有函数,私有函数假设在调用互斥时持有互斥,并在调用私有实现之前将普通QMutex锁定在公共函数中。
2、或者使用递归互斥,这样第一个公共函数已经锁定了互斥,而第二个公共函数希望这样做,这并不重要。

QRecursiveMutex::QRecursiveMutex()
//构造一个新的递归互斥。互斥锁是在未锁定状态下创建的。
QRecursiveMutex::~QRecursiveMutex()
//销毁递归锁
  • 34
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值