C++11并发编程(二):互斥锁

1、了解线程的同步和互斥

       同步是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。

       最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据

  互斥是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。

       最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源

2、互斥锁(mutex)

       在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

多个线程同时访问共享资源的时候需要用到互斥量,当一个线程锁住了互斥量后,其他线程必须等待这个互斥量解锁后才能访问它。

在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问。

只要某一个线程上锁了,那么就会强行霸占公共资源的访问权,其他线程试图访问资源会被阻塞,直到这个线程解锁了。

互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

例如下面这个例子中,g_page_mutex这把锁保护了g_page这个map。

#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include <map>

using namespace std;

map<string, string> g_page;
mutex g_page_mutex;

void save_page(const string& url)
{
    this_thread::sleep_for(chrono::seconds(2));
    string result = "fake content";

    g_page_mutex.lock();
    g_page[url] = result;
    g_page_mutex.unlock();
}

int main(int argc, char *argv[])
{
    thread t1(save_page, "http://foo");
    thread t2(save_page, "http://bar");
    t1.join();
    t2.join();

    for (const auto &pair : g_page)
    {
        cout << pair.first << "=>" << pair.second << endl;
    }

    return 0;
}

结果:

3lock_guard

但是上面手动上锁解锁的操作有个隐患是,如果上锁和解锁中间的代码出现了问题,那么这把锁就一直无法解开,成为了死锁。

c++11也提供了一种更安全更方便的上锁和解锁的方式,就是lock_guard这个类。从名字可以看出,这是一个监视锁的类。

它通过将对象构建与lock绑定,对象析构与unlock绑定的方式实现更方便的锁操作。

#include <iostream>
#include <thread>
#include <mutex>
#include <string>
#include <map>

using namespace std;

map<string, string> g_page;
mutex g_page_mutex;

void save_page(const string& url)
{
    this_thread::sleep_for(chrono::seconds(2));
    string result = "fake content";

    lock_guard<mutex> lc(g_page_mutex);

    //或者也可以写成
    //lock(g_page_mutex);// 此函数有2个参数,此处仅作演示
    //lock_guard<mutex> lc(g_page_mutex, adopt_lock);
    //或者也可以写成
    //lock_guard<mutex> lc(g_page_mutex, defer_lock);
    //lock(g_page_mutex);// 此函数有2个参数,此处仅作演示

    g_page[url] = result;
}

int main(int argc, char *argv[])
{
    thread t1(save_page, "http://foo");
    thread t2(save_page, "http://bar");
    t1.join();
    t2.join();

    for (const auto &pair : g_page)
    {
        cout << pair.first << "=>" << pair.second << endl;
    }

    return 0;
}

结果:

4、unique_lock

上文介绍的lock_guard类最大的缺点也是简单,没有给程序员提供足够的灵活度,因此C++11定义了另一个unique_guard类。

这个类和lock_guard类似,也很方便线程对互斥量上锁,但它提供了更好的上锁和解锁控制。

互斥锁保证了线程间的公共资源访问,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁。

这一点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的用武之地。

 

参考链接:

https://www.cnblogs.com/corineru/p/10847930.html

https://blog.csdn.net/chm880910/article/details/86546341

 


若对你有帮助,欢迎点赞、收藏、评论,你的支持就是我的最大动力!!!

同时,阿超为大家准备了丰富的学习资料,欢迎关注公众号“超哥学编程”,即可领取。

本文涉及工程代码,公众号回复:02Lock,即可下载

在这里插入图片描述

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

百里杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值