std::lock_guard 引起的思考

std::lock_guard 引起的思考


版权声明:本文为 cheng-zhi 原创文章,可以随意转载,但必须在明确位置注明出处!

从哪里来的思考?

最近在项目总结过程中,发现项目大量使用了 std::lock_guard 这个模板类,仔细分析后发现这个类牵扯到了很多重要的计算机基础,例如:多线程,互斥,锁等等,这里便记录下来,也算是一次简单的总结。

std::lock_guard 简介

这个类是一个互斥量的包装类,用来提供自动为互斥量上锁和解锁的功能,简化了多线程编程,用法如下:

#include <mutex>

std::mutex kMutex;

void function() {
  // 构造时自动加锁
  std::lock_guard<std::mutex> (kMutex);
  
  // 离开局部作用域,析构函数自动完成解锁功能
}

用法非常简单,只需在保证线程安全的函数开始处加上一行代码即可,其他的都在这个类的构造函数和析构函数中自动完成。

如何自动完成?其实 Just so so ...

实现 my_lock_guard

这是自己实现的一个 lock_guard,就是在构造和析构中完成加锁和解锁的操作,之所以会自动完成,是因为离开函数作用域会导致局部变量析构函数被调用,而我们又是手动构造了 lock_guard,因此这两个函数都是自动被调用的。

namespace myspace {
    template<typename T> class my_lock_guard {
    public:
        // 在 std::mutex 的定义中,下面两个函数被删除了
        // mutex(const mutex&) = delete;
        // mutex& operator=(const mutex&) = delete;
        // 因此这里必须传递引用
        my_lock_guard(T& mutex) :mutex_(mutex){
            // 构造加锁
            mutex_.lock();
        }

        ~my_lock_guard() {
            // 析构解锁
            mutex_.unlock();
        }
    private:
        // 不可赋值,不可拷贝
        my_lock_guard(my_lock_guard const&);
        my_lock_guard& operator=(my_lock_guard const&);
    private:
        T& mutex_;
    };

};

要注意的是这个类官方定义是不可以赋值和拷贝,因此需要私有化 operator =copy 这两个函数。

什么是 std::mutex ?

如果你细心可以发现,不管是 std::lock_guard,还是my_lock_guard,都使用了一个 std::mutex 作为构造函数的参数,这是因为我们的 lock_guard 只是一个包装类,而实际的加锁和解锁的操作都还是 std::mutex 完成的,那什么是 std::mutex 呢?

std::mutex 其实是一个用于保护共享数据不会同时被多个线程访问的类,它叫做互斥量,你可以把它看作一把锁,它的基本使用方法如下:

#include <mutex>

std::mutex kMutex;

void function() {
  //加锁
  kMutex.lock();
  //kMutex.try_lock();

  //do something that is thread safe...
  
  // 离开作用域解锁
  kMutex.unlock();
}

前面都提到了这个概念,那么什么是锁,有啥用处?

什么是锁?

锁是用来保护共享资源(变量或者代码)不被并发访问的一种方法,它只是方法,实际的实现就是 std::mutex 等等的类了。

可以简单的理解为:

  1. 当前线程访问一个变量之前,将这个变量放到盒子里锁住,并且当前线程拿着钥匙。这样一来,如果有其他的线程也要访问这个变量,则必须等待当前线程将盒子解锁之后才能访问,之后其他线程在访问这个变量之前也将会再次锁住这个变量。

  2. 当前线程执行完后,就将该盒子解锁,这样其他的线程就可以拿到盒子的钥匙,并再次加锁访问这个变量了。

这样就保证了同一时刻只有一个线程可以访问共享资源,解决了简单的线程安全问题。

什么,你还没有遇到过线程安全问题?下面开始我的表演...

一个简单的线程安全的例子

这个例子中,主线程开启了 2 个子线程,每个子线程都修改共享的全局变量 kData,如果没有增加必要的锁机制,那么每个子线程打印出的 kData 就可能会出错。

这里使用了 3 种不同的加锁方法来解决:

  1. 使用 std::lock_guard
  2. 使用 std::mutex 实现原生的加锁
  3. 使用自己的 myspace::my_lock_guard
#include <iostream>
#include <mutex>
#include <thread>

// 两个子线程共享的全局变量
int kData = 0;

// std::mutex 提供了一种防止共享数据被多个线程并发访问的简单同步方法
// 调用线程可以通过 lock 和 try_lock 来获取互斥量,使用 unlock() 释放互斥量
std::mutex kMutex;


void increment() {
    // 1.创建一个互斥量的包装类,用来自动管理互斥量的获取和释放
    // std::lock_guard<std::mutex> lock(kMutex);
    
    // 2.原生加锁
    // kMutex.lock();

    // 3.自己实现的 std::mutex 的包装类
    myspace::my_lock_guard<std::mutex> lock(kMutex);
    
    for (int i = 0; i < 10; i++) {
        // 打印当前线程的 id : kData
        std::cout << std::this_thread::get_id() 
                  << ":" << kData++ << std::endl;
    }
    
    // 2. 原生解锁  
    //kMutex.unlock();
    
    // 离开局部作用域,局部锁解锁,释放互斥量
    
}


int main()
{
    // 打印当前函数名
    std::cout << __FUNCTION__ << ":" << kData << std::endl;

    // 开启两个线程
    std::thread t1(increment);
    std::thread t2(increment);

    // 主线程等待这两个线程完成操作之后再退出
    t1.join();
    t2.join();

    // 防止立刻退出
    getchar();
    return 0;

}

注意:在 vs 中编译这段代码。

结果分析

为什么不加锁的结果会出错?

首先线程是一种轻量级的进程,也存在调度,假设当前 CPU 使用的是基于时间片的轮转调度算法,为每个进程分配一段可执行的时间片,因此每个线程都得到一段可以执行的时间(这里只是简单概括,仔细研究其实是有点复杂的,涉及到内核线程和用户线程,这里就不多说了,不是这里讨论的重点),这就导致子线程 1 在修改并打印 kData 的时候,子线程 1 的时间片用完了,CPU 切换到子线程 2 去修改并打印 kData,这就导致了最终的打印结果不是预先的顺序,就是这个原理,简单的理解是不难的。

原文地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容