文章目录

  • 1.简介
  • 2.std::mutex
  • 3.线程同步
  • 4.std::lock_guard
  • 5.std::recursive_mutex-少用
  • 6.std::timed_mutex

1.简介

进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写,对于后两种情况来说,如果不做任何的人为干涉就会出现各种各样的错误数据。

  • 这是因为线程在运行的时候需要先得到 CPU 时间片,时间片用完之后需要放弃已获得的 CPU 资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的,就如同下图中的这个例子一样,理想很丰满现实却很残酷。

解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在 C++11 中一共提供了四种互斥锁:

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

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

2.std::mutex

不论是在 C 还是 C++ 中,进行线程同步的处理流程基本上是一致的,C++ 的 mutex 类提供了相关的 API 函数:

成员函数

  • lock() 函数用于给临界区加锁,并且只能有一个线程获得锁的所有权,它有阻塞线程的作用,函数原型如下:
void lock();
  • 独占互斥锁对象有两种状态:锁定和未锁定。

如果互斥锁是打开的,调用 lock() 函数的线程会得到互斥锁的所有权,并将其上锁,其它线程在调用该函数的时候由于得不到互斥锁的所有权,就会被 lock() 函数阻塞。

当拥有互斥锁所有权的线程将互斥锁解锁,此时被 lock() 阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。

  • 除了使用 lock() 还可以使用 try_lock() 获取互斥锁的所有权并对互斥锁加锁,函数原型如下:
bool try_lock();

二者的区别在于 try_lock() 不会阻塞线程,lock() 会阻塞线程:

  • 如果互斥锁是未锁定状态,得到了互斥锁所有权并加锁成功,函数返回 true
  • 如果互斥锁是锁定状态,无法得到互斥锁所有权加锁失败,函数返回 false

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

  • 该函数的函数原型如下:
void unlock();

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

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

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

  • 当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。
  • 死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。

3.线程同步

  • eg:我们让两个线程共同操作同一个全局变量,二者交替数数,将数值存储到这个全局变量里边并打印出来
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
using namespace std;int g_num = 0;  // 为 g_num_mutex 所保护
mutex g_num_mutex;void slow_increment(int id)
{for (int i = 0; i < 3; ++i){g_num_mutex.lock();++g_num;cout << id << " => " << g_num << endl;g_num_mutex.unlock();this_thread::sleep_for(chrono::seconds(1));}
}int main()
{thread t1(slow_increment, 0);thread t2(slow_increment, 1);t1.join();t2.join();
}
  • 测试:
  • 解释:

两个子线程执行的任务的一样的(其实也可以不一样,不同的任务中也可以对共享资源进行读写操作),在任务函数中把与全局变量相关的代码加了锁,两个线程只能顺序访问这部分代码(如果不进行线程同步打印出的数据是混乱且无序的)。

在所有线程的任务函数执行完毕之前,互斥锁对象是不能被析构的,一定要在程序中保证这个对象的可用性。

互斥锁的个数和共享资源的个数相等,也就是说每一个共享资源都应该对应一个互斥锁对象。互斥锁对象的个数和线程的个数没有关系。

4.std::lock_guard

lock_guard 是 C++11 新增的一个模板类,使用这个类,可以简化互斥锁 lock() 和 unlock() 的写法,同时也更安全。

  • 这个模板类的定义和常用的构造函数原型如下:
// 类的定义,定义于头文件 <mutex>
template< class Mutex >
class lock_guard;// 常用构造函数
explicit lock_guard( mutex_type& m );

lock_guard 在使用上面提供的这个构造函数构造对象时,会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记 unlock() 操作而导致线程死锁。

  • lock_guard 使用了 RAII 技术,就是在类构造函数中分配资源,在析构函数中释放资源,保证资源出了作用域就释放。

  • eg:使用 lock_guard 对上面的例子进行修改,代码如下

void slow_increment(int id)
{for (int i = 0; i < 3; ++i) {// 使用哨兵锁管理互斥锁lock_guard<mutex> lock(g_num_mutex);++g_num;cout << id << " => " << g_num << endl;this_thread::sleep_for(chrono::seconds(1));}
}
  • 测试:

  • 说明:
    通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,但是这种方式也有弊端,在上面的示例程序中整个for循环的体都被当做了临界区,多个线程是线性的执行临界区代码的,因此临界区越大程序效率越低, 还是需要根据实际情况选择最优的解决方案。

5.std::recursive_mutex-少用

递归互斥锁 std::recursive_mutex 允许同一线程多次获得互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题

  • eg:在下面的例子中使用独占非递归互斥量会发生死锁
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;struct Calculate
{Calculate() : m_i(6) {}void mul(int x){lock_guard<mutex> locker(m_mutex);m_i *= x;}void div(int x){lock_guard<mutex> locker(m_mutex);m_i /= x;}void both(int x, int y){lock_guard<mutex> locker(m_mutex);mul(x);div(y);}int m_i;mutex m_mutex;
};int main()
{Calculate cal;cal.both(6, 3);return 0;
}
  • 测试:
  • 解释:上面的程序中执行了 cal.both(6, 3); 调用之后,程序就会发生死锁,在 both() 中已经对互斥锁加锁了,继续调用 mult() 函数,已经得到互斥锁所有权的线程再次获取这个互斥锁的所有权就会造成死锁(在 C++ 中程序会异常退出,使用 C 库函数会导致这个互斥锁永远无法被解锁,最终阻塞所有的线程)。

要解决这个死锁的问题,一个简单的办法就是使用递归互斥锁 std::recursive_mutex,它允许一个线程多次获得互斥锁的所有权。

  • 修改之后的代码如下:

  • 测试:

虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:

  • 使用递归互斥锁的场景往往都是可以简化的,使用递归互斥锁很容易放纵复杂逻辑的产生,从而导致bug的产生
  • 递归互斥锁比非递归互斥锁效率要低一些。
  • 递归互斥锁虽然允许同一个线程多次获得同一个互斥锁的所有权,但最大次数并未具体说明,一旦超过一定的次数,就会抛出std::system错误。

6.std::timed_mutex

std::timed_mutex 是超时独占互斥锁, 主要是在获取互斥锁资源时增加了超时等待功能,因为不知道获取锁资源需要等待多长时间,为了保证不一直等待下去,设置了一个超时时长,超时后线程就可以解除阻塞去做其他事情了。

  • std::timed_mutex 比 std::_mutex 多了两个成员函数:try_lock_for() 和 try_lock_until():
void lock();
bool try_lock();
void unlock();// std::timed_mutex比std::_mutex多出的两个成员函数
template <class Rep, class Period>bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);template <class Clock, class Duration>bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);
  • try_lock_for 函数是当线程获取不到互斥锁资源的时候,让线程阻塞一定的时间长度
  • try_lock_until 函数是当线程获取不到互斥锁资源的时候,让线程阻塞到某一个指定的时间点
  • 关于两个函数的返回值:当得到互斥锁的所有权之后,函数会马上解除阻塞,返回 true,如果阻塞的时长用完或者到达指定的时间点之后(超时),函数也会解除阻塞,返回 false
  • eg:std::timed_mutex 的使用:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;timed_mutex g_mutex;void work()
{chrono::seconds timeout(1);while (true){// 通过阻塞一定的时长来争取得到互斥锁所有权if (g_mutex.try_lock_for(timeout)){cout << "当前线程ID: " << this_thread::get_id()<< ", 得到互斥锁所有权..." << endl;// 模拟处理任务用了一定的时长this_thread::sleep_for(chrono::seconds(10));// 互斥锁解锁g_mutex.unlock();break;}else{cout << "当前线程ID: " << this_thread::get_id()<< ", 没有得到互斥锁所有权..." << endl;// 模拟处理其他任务用了一定的时长this_thread::sleep_for(chrono::milliseconds(50));}}
}int main()
{thread t1(work);thread t2(work);t1.join();t2.join();return 0;
}
  • 测试:
  • 解释:通过一个 while 循环不停的去获取超时互斥锁的所有权,如果得不到就阻塞 1 秒钟,1 秒之后如果还是得不到阻塞 50 毫秒,然后再次继续尝试,直到获得互斥锁的所有权,跳出循环体。

关于递归超时互斥锁 std::recursive_timed_mutex 的使用方式和 std::timed_mutex 是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而 std::timed_mutex 只允许线程获取一次互斥锁所有权。

  • 另外,递归超时互斥锁 std::recursive_timed_mutex 也拥有和 std::recursive_mutex 一样的弊端,不建议频繁使用。

  • 参考:C++ 线程同步之互斥锁

C++ 线程同步之互斥锁相关推荐

  1. linux线程同步之互斥锁——linux的关键区域

    在windows中,为了让多个线程达到同步的目的,在对于全局变量等大家都要用的资源的使用上,通常得保证同时只能由一个线程在用,一个线程没有宣布对它的释放之前,不能够给其他线程使用这个变量.在windo ...

  2. java 信号量 互斥锁_线程同步(互斥锁与信号量的作用与区别)

    "信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里).而互斥锁是用在多线程多任务互斥的,一 ...

  3. c/c++:线程同步(互斥锁、死锁、读写锁、条件变量、生产者和消费者模型、信号量)

    目录 1. 概念 2. 互斥锁 3. 死锁 4. 读写锁 5. 条件变量 5.1 生产者和消费者模型 6. 信号量 1. 概念 线程同步: > 当有一个线程在对内存进行操作时,其他线程都不可以对 ...

  4. Linux线程同步(二)---互斥锁实现线程同步

    一 why 先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题. 在博客&l ...

  5. Linux线程同步(三)---互斥锁源码分析

    先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题. 一 源码分析 1.li ...

  6. python线程同步锁_Python实现的多线程同步与互斥锁功能示例

    本文实例讲述了Python实现的多线程同步与互斥锁功能.分享给大家供大家参考,具体如下: #! /usr/bin/env python #coding=utf-8 import threading i ...

  7. Linux多线程编程---线程间同步(互斥锁、条件变量、信号量和读写锁)

    本篇博文转自http://zhangxiaoya.github.io/2015/05/15/multi-thread-of-c-program-language-on-linux/ Linux下提供了 ...

  8. python同步锁和互斥锁的区别_Python实现的多线程同步与互斥锁功能示例

    本文实例讲述了Python实现的多线程同步与互斥锁功能.分享给大家供大家参考,具体如下: #! /usr/bin/env python #coding=utf-8 import threading i ...

  9. 信号灯文件锁linux线程,linux——线程同步(互斥量、条件变量、信号灯、文件锁)...

    一.说明 linux的线程同步涉及: 1.互斥量 2.条件变量 3.信号灯 4.文件读写锁 信号灯很多时候被称为信号量,但个人仍觉得叫做信号灯比较好,因为可以与"SYSTEM V IPC的信 ...

最新文章

  1. 使用VS搭建三层结构
  2. 理解GO CONTEXT机制
  3. ECCV 2020 Spotlight | CFBI:前背景整合的协作式视频目标分割
  4. linux php运行用户名和密码,Linux实例(一)使用用户名密码验证连接Linux
  5. 剑指offer面试题[58]-二叉树的下一个结点
  6. mysql root远程访问权限_解决Navicat连接MySQL数据库报错问题
  7. 读《非暴力沟通》马歇尔·卢森堡
  8. “U盘杀手”出现新变种 提醒用户小心谨防
  9. MySQL 数据库基础知识(系统化一篇入门)
  10. 综合项目之闪讯破解(三)之 如何用C++实现PPPOE拨号
  11. kangle安装php7.0_Kangle Easypanel面板 PHP多版本切换 安装图文教程
  12. DDR功能点 ODT ZQ校准
  13. Excel 文件怎么批量插入首页、扉页、尾页?怎么将某个 Excel 文件批量插入到其它 Excel 文件的指定位置?
  14. 我们怎么保证软件开发的质量?
  15. alios下载_AliOS-Things ESP8266 编译下载
  16. Java9、10、11、12、13、14、15、16、17个版本新特性
  17. 终于,我也出了篇R语言入门手册!
  18. 随身理财专家“挖财”推iPad应用,新增帐号对比功能
  19. MATLAB安装时为英文如何切换中文
  20. 测试吃鸡游戏帧数软件,高频内存吃鸡、CSGO帧数提高多少?这一测试告诉你

热门文章

  1. flutter- ListTile leading加载本地图标
  2. 进阶的爬虫系列 ——不得不说的贴吧爬取术
  3. 海马汽车经销商管理系统技术解析(十)预约配件资源释放
  4. 纯前端实现:下载电子书 手机观看
  5. 法语初级学习笔记-03-疑问句
  6. 推荐三款高级可视化工具,解决90%的数据可视化大屏需求
  7. PhysX3.4文档(11) -- GPU Rigid Bodies
  8. rEFInd引导Win10+Ubuntu+Deepin+macOS+Phoenix+Fyde+cent+openSUSE+Kylin+ChromeOS+RedFlag)时menuentry的详细配置
  9. MySQL从外部导入数据库教程
  10. VS如何让代码显示行号