[C++11 多线程同步] --- 互斥锁

Posted Overboom

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++11 多线程同步] --- 互斥锁相关的知识,希望对你有一定的参考价值。

1 四种互斥锁

在 C++11 中一共提供了四种互斥锁:

std::mutex:独占的互斥锁,不能递归使用
std::timed_mutex:带超时的独占互斥锁,不能递归使用
std::recursive_mutex:递归互斥锁,不带超时功能
std::recursive_timed_mutex:带超时的递归互斥锁

互斥锁在有些资料中也被称之为互斥量,二者是一个东西。

互斥锁是一种简单的加锁的方法来控制对共享资源的访问。只要某一个线程上锁了,那么就会强行霸占公共资源的访问权,其他线程试图访问资源会被阻塞,直到这个线程解锁了。互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

2 mutex

2.1 mutex的成员函数

2.1.1 lock()

lock() 函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用,函数原型如下:

void lock();

2.1.2 try_lock()

try_lock()函数原型如下:

bool try_lock();

try_lock() 不会阻塞线程,lock() 会阻塞线程,try_lock在调用时:
如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回 true
如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回 false

2.1.3 unlock()

unlock()函数原型如下:

bool unlock();

当互斥锁被锁定之后可以通过 unlock() 进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。

通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:

  1. 找到多个线程操作的共享资源(全局变量、堆内存、类成员变量等),也可以称之为临界资源
  2. 找到和共享资源有关的上下文代码,也就是临界区(下图中的黄色代码部分)
  3. 在临界区的上边调用互斥锁类的 lock() 方法
  4. 在临界区的下边调用互斥锁的 unlock() 方法

线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。

上一篇文章中,两个线程分别对一个数加100次,由于没有使用线程同步机制,得到错误的结果,这里用mutex进行线程同步,示例代码如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <unistd.h>

using namespace std;
int share = 0;  //共享变量
mutex g_mutex;

void thread1()

    for (int i = 0; i < 100; i++)
    
		g_mutex.lock();
		int tmp = share;
        tmp++;
        usleep(10);
		share = tmp;
		g_mutex.unlock();
        cout << "thread1: share is " << share << endl;
		
    

void thread2()

    for (int i = 0; i < 100; i++)
    
		g_mutex.lock();
		int tmp = share;
        tmp++;
        usleep(200);
		share = tmp;
		g_mutex.unlock();
		cout << "thread2: share is " << share << endl;

    


int main()

    thread task1(thread1); 
    thread task2(thread2); 
    task1.join();
    task2.join();
   

3 lock_guard()

lock_guard 是 C++11 新增的一个模板类,使用这个类,可以简化互斥锁 lock() 和 unlock() 的写法,同时也更安全。
lock_guard 在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记 unlock() 操作而导致线程死锁。lock_guard 使用了 RAII 技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。

4 unique_lock()

互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁。这一点lock_guard做的不好,不够灵活,lock_guard只能保证在析构的时候执行解锁操作,lock_guard本身并没有提供加锁和解锁的接口,但是有些时候会有这种需求。看下面的例子。

class LogFile 
    std::mutex _mu;
    ofstream f;
public:
    LogFile() 
        f.open("log.txt");
    
    ~LogFile() 
        f.close();
    
    void shared_print(string msg, int id) 
        
            std::lock_guard<std::mutex> guard(_mu);
            //do something 1
        
        //do something 2
        
            std::lock_guard<std::mutex> guard(_mu);
            // do something 3
            f << msg << id << endl;
            cout << msg << id << endl;
        
    

;

上面的代码中,一个函数内部有两段代码需要进行保护,这个时候使用lock_guard就需要创建两个局部对象来管理同一个互斥锁(其实也可以只创建一个,但是锁的力度太大,效率不行),修改方法是使用unique_lock。它提供了lock()和unlock()接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁(lock_guard就一定会解锁)。上面的代码修改如下:

class LogFile 
    std::mutex _mu;
    ofstream f;
public:
    LogFile() 
        f.open("log.txt");
    
    ~LogFile() 
        f.close();
    
    void shared_print(string msg, int id) 

        std::unique_lock<std::mutex> guard(_mu);
        //do something 1
        guard.unlock(); //临时解锁

        //do something 2

        guard.lock(); //继续上锁
        // do something 3
        f << msg << id << endl;
        cout << msg << id << endl;
        // 结束时析构guard会临时解锁
        // 这句话可要可不要,不写,析构的时候也会自动执行
        // guard.ulock();
    

;

上面的代码可以看到,在无需加锁的操作时,可以先临时释放锁,然后需要继续保护的时候,可以继续上锁,这样就无需重复的实例化lock_guard对象,还能减少锁的区域。同样,可以使用std::defer_lock设置初始化的时候不进行默认的上锁操作:

void shared_print(string msg, int id) 
    std::unique_lock<std::mutex> guard(_mu, std::defer_lock);
    //do something 1

    guard.lock();
    // do something protected
    guard.unlock(); //临时解锁

    //do something 2

    guard.lock(); //继续上锁
    // do something 3
    f << msg << id << endl;
    cout << msg << id << endl;
    // 结束时析构guard会临时解锁

这样使用起来就比lock_guard更加灵活!然后这也是有代价的,因为它内部需要维护锁的状态,所以效率要比lock_guard低一点,在lock_guard能解决问题的时候,就是用lock_guard,反之,使用unique_lock。

后面在学习条件变量的时候,还会有unique_lock的用武之地。

另外注意,unique_lock和lock_guard都不能复制,lock_guard不能移动,但是unique_lock可以!

// unique_lock 可以移动,不能复制
std::unique_lock<std::mutex> guard1(_mu);
std::unique_lock<std::mutex> guard2 = guard1;  // error
std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok

// lock_guard 不能移动,不能复制
std::lock_guard<std::mutex> guard1(_mu);
std::lock_guard<std::mutex> guard2 = guard1;  // error
std::lock_guard<std::mutex> guard2 = std::move(guard1); // error

5 recursive_mutex和recursive_timed_mutex

目前工作中没有碰到这种稍复杂的需求,后面碰到了再研究。。。

以上是关于[C++11 多线程同步] --- 互斥锁的主要内容,如果未能解决你的问题,请参考以下文章

[C++11 多线程同步] --- 互斥锁

如何在c中正确同步多线程与互斥锁?

Linux 线程同步都有哪些方法?

多线程的同步和互斥有啥区别

多线程安全----同步锁(互斥锁)

悲观的并发策略——synchronized互斥锁