Studying-多线程学习Part2 - 互斥量死锁、lock_guard 与 unique_lock、call_once与其使用场景

news2024/10/5 0:19:32

来源:多线程学习

互斥量死锁

假设有两个线程 T1 和 T2,它们需要对两个互斥量 mtx1 和 mtx2 进行访问,而且需要按照以下顺序获取互斥量的所有权:

  • T1 先获取 mtx1 的所有权,再获取 mtx2 的所有权。
  • T2 先获取 mtx2 的所有权,再获取 mtx1 的所有权。

如果两个线程同时执行,就会出现死锁问题。因为 T1 获取了 mtx1 的所有权,但是无法获取 mtx2 的所有权,而 T2 获取了 mtx2 的所有权,但是无法获取 mtx1 的所有权,两个线程互相等待对方释放互斥量,导致死锁。 

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int share_data = 0;
mutex mtx1;
mutex mtx2;
void fun1() {
	for (int i = 0; i < 100000; ++i) {
		mtx1.lock();
		mtx2.lock();
		mtx1.unlock();
		mtx2.unlock();
	}
}

void fun2() {
	for (int i = 0; i < 100000; ++i) {
		mtx2.lock();
		mtx1.lock();
		mtx2.unlock();
		mtx1.unlock();
	}
}

int main() {
	thread t1(fun1);
	thread t2(fun2);
	t1.join();
	t2.join();
	cout << share_data << endl;
	return 0;
}

解决该问题的方法,可以让两个线程按照相同的顺序获取互斥量的所有权。例如,都先获取 mtx1 的所有权,再获取 mtx2 的所有权,或者都先获取 mtx2 的所有权,再获取 mtx1 的所有权。这样就可以避免死锁问题。

原因在于每个程序在运行的时候,都要先判断mtx1是否被占用,如果被占用就不能继续往下了,也就取不到mtx2了,而如果没有被占用则就可以继续往下。

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int share_data = 0;
mutex mtx1;
mutex mtx2;
void fun1() {
	for (int i = 0; i < 100000; ++i) {
		mtx1.lock();
		mtx2.lock();
		mtx1.unlock();
		mtx2.unlock();
	}
}

void fun2() {
	for (int i = 0; i < 100000; ++i) {
		mtx1.lock();
		mtx2.lock();
		mtx2.unlock();
		mtx1.unlock();
	}
}

int main() {
	thread t1(fun1);
	thread t2(fun2);
	t1.join();
	t2.join();
	cout << share_data << endl;
	return 0;
}

lock_guard 与 unique_lock

lock_guard

lock_guard是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。 

lock_guard的特点是:

  • 当构造函数被调用时,该互斥量会被自动锁定。

  • 当析构函数被调用时,该互斥量会被自动解锁。

  • lock_guard对象不能复制或移动,因此它只能在局部作用域中使用。

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int share_data = 0;
mutex mtx;
void fun() {
	for (int i = 0; i < 1000000; ++i) {
		//创建即加锁
		lock_guard<mutex> lg(mtx);
		share_data += 1;
	}
}

int main() {
	thread t1(fun);
	thread t2(fun);
	t1.join();
	t2.join();
	cout << share_data << endl;
	return 0;
}

lock_guard还有第二种构造函数,除了传递mtx以外,还可以传递一个参数adopt_lock,表示之前以及上过锁了。

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int share_data = 0;
mutex mtx;
void fun() {
	for (int i = 0; i < 1000000; ++i) {
		//创建即加锁
		mtx.lock();
		lock_guard<mutex> lg(mtx, adopt_lock);
		share_data += 1;
	}
}

int main() {
	thread t1(fun);
	thread t2(fun);
	t1.join();
	t2.join();
	cout << share_data << endl;
	return 0;
}

unique_lock 

unique_lock 是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。 

unique_lock提供了以下几个成员函数:

  • lock():尝试对互斥量进行加锁操作(这是个手动加锁的操作),如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
  • try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回false,否则返回true,和lock()不同的是,使用try_lock()不会发生堵塞。
  • try_lock_for(const chrono::duration(Rep, Period>& rel_time) :与上一个函数相比,增加了一个时间参数,也是尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被另一个线程解锁,当前线程成功加锁,或者超过了给定的等待时间。
  • try_lock_until(const chrono::time_point<Clock, Duration>& abs_time):和上一个函数效果类似,不同的在于,增加的参数是一个时间点,效果是对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
  • unlock():对互斥量进行解锁操作(手动解锁)
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int share_data = 0;
//由于后续要使用到时间操作,因此这里我们需要使用到时间锁
timed_mutex mtx;
void fun() {
	for (int i = 0; i < 10; ++i) {
		//增加defer_lock参数,表示创建锁,但是不进行枷锁操作
		unique_lock<timed_mutex> lg(mtx, defer_lock); 
		//try_lock_for返回的是一个bool值,成功加锁返回true,否则返回false
		if (lg.try_lock_for(chrono::seconds(1))) {
			share_data += 1;
			//表示当前线程休眠2s
			this_thread::sleep_for(chrono::seconds(2));
		}
	}
}

int main() {
	thread t1(fun);
	thread t2(fun);
	t1.join();
	t2.join();
	cout << share_data << endl;
	return 0;
}

unique_lock提供了以下几个构造函数:

  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的unique_lock对象。
  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量m 进行初始化,并默认对该互斥量进行枷锁操作。(只支持显示构造)
  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量m 进行初始化,但是不对该互斥量进行加锁操作。
  • unique_lock(mutex_type& m, try_lock_t) noexcept:构造函数,使用给定的互斥量m 进行初始化,并尝试对该互斥量进行加锁,如果加锁失败,则创建的unqiue_lock 对象不与任何互斥量关联。
  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量m 进行初始化,并假设互斥量已经被当前线程成功加锁。

总结:相比于lock_guard,unqiue_lock使用上非常灵活方便。


call_once与其使用场景 

该使用场景主要就是单例设计模式。单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。

代码实现:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

class Log {
public:
	Log() {};
	//单例模式,类只能有一个对象,因此我们需要禁用其拷贝构造和等于号
	Log(const Log& log) = delete;
	Log& operator=(const Log& log) = delete;

	//全局只需要一个,我们可以采用静态构造的方法
	//static Log& GetInstance() {
	//	static Log log; //饿汉单例模式
	//	return log;
	//}

	static Log& GetInstance() {
		static Log* log = nullptr; //懒汉模式

		if (!log) log = new Log;
		return *log;
	}

	void PrintLog(string msg) {
		cout << __TIME__ << " " << msg << endl;
	}
};

int main() {
	Log::GetInstance().PrintLog("这里有问题");
	return 0;
}

上述中,我们创建了一个日志类,并给了懒汉模式和饿汉模式两种静态构造方法。 由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。

但是,该实现并不是线程安全的。如果多个线程同时调用 getInstance() 函数,可能会导致多个对象被创建,从而违反了单例模式的要求。

为例解决该问题,我们可以使用call_once来实现一次性初始化,从而确保单例实例只会被创建一次。

call_once函数的原型为:void call_once(std::once_flag& flag, Callable&& func, Args&&... args);

其中`flag` 是一个 `std::once_flag` 类型的对象,用于标记函数是否已经被调用;`func` 是需要被调用的函数或可调用对象;`args` 是函数或可调用对象的参数。

`std::call_once` 的作用是,确保在多个线程中同时调用 `call_once` 时,只有一个线程能够成功执行 `func` 函数,而其他线程则会等待该函数执行完成。

class Singleton {
public:
    static Singleton& getInstance() {
            std::call_once(m_onceFlag, &Singleton::init);
            return *m_instance;
    }    
    void setData(int data) {
        m_data = data;
    }    
    int getData() const {        
    return m_data;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;    
    static void init() {
        m_instance.reset(new Singleton);
    }    
    static std::unique_ptr<Singleton> m_instance;    
    static std::once_flag m_onceFlag;    
    int m_data = 0;
};
std::unique_ptr<Singleton> Singleton::m_instance;
std::once_flag Singleton::m_onceFlag;

在这个实现中,我们使用了一个静态成员变量 m_instance 来存储单例实例,使用了一个静态成员变量 m_onceFlag 来标记初始化是否已经完成。在 getInstance() 函数中,我们使用 std::call_once 来调用 init() 函数,确保单例实例只会被创建一次。在 init() 函数中,我们使用了 std::unique_ptr 来创建单例实例。

使用 std::call_once 可以确保单例实例只会被创建一次,从而避免了多个对象被创建的问题。此外,使用 std::unique_ptr 可以确保单例实例被正确地释放,避免了内存泄漏的问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188971.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

deploy thingsboard

ThingsBoard部署 平台&#xff1a;windows10&#xff0c;idea2022&#xff0c;postgres15 maven仓库 进入thingsboard源码下载目录: 主要执行以下两个命令&#xff1a; mvn编译&#xff1a; mvn clean install -Dmaven.test.skiptrue编译报错时&#xff1a; 清除java进程 t…

计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

UART通信—基于江科大源码基础进行的改进和解析

我就不讲理论了&#xff0c;CSDN上大佬属实多&#xff0c;我就只讲代码了&#xff0c;串口的基本理论&#xff0c;大家去看其他大佬写的吧 一、源文件的组成 1、包含的头文件 stm32f10x.h 是STM32F10x系列微控制器的标准外设库&#xff08;Standard Peripheral Library&…

C语言基础(7)之操作符(1)(详解)

目录 1. 各种操作符介绍 1.1 操作符汇总表 2. 移位操作符 2.1 移位操作符知识拓展 —— 原码、反码、补码 2.2 移位操作符讲解 2.2.1 右移操作符 ( >> ) 2.2.2 左移操作符 ( << ) 3. 位操作符 3.1 & (按位与) 3.2 | (按位或) 3.3 ^ (按位异或) 3.4…

【AI学习】Mamba学习(二):线性注意力

上一篇《Mamba学习&#xff08;一&#xff09;&#xff1a;总体架构》提到&#xff0c;Transformer 模型的主要缺点是&#xff1a;自注意力机制的计算量会随着上下文长度的增加呈平方级增长。所以&#xff0c;许多次二次时间架构&#xff08;指一个函数或算法的增长速度小于二次…

C++ 多态:重塑编程效率与灵活性

目录 多态的概念 多态的定义及实现 多态的构成条件 虚函数 虚函数的重写 虚函数重写的两个例外&#xff1a; 1. 协变(基类与派生类虚函数返回值类型不同) 2. 析构函数的重写(基类与派生类析构函数的名字不同&#xff09; 析构函数要不要定义成虚函数&#xff1f;&…

绝对值得收藏!分享7款ai写作论文免费一键生成网站

在当前的学术研究和写作过程中&#xff0c;AI写作工具已经成为了许多研究者和学生的重要助手。这些工具不仅能够提高写作效率&#xff0c;还能帮助生成高质量的论文内容。以下是七款免费的AI写作论文生成器&#xff0c;其中特别推荐千笔-AIPassPaper。 1.千笔-AIPassPaper 千…

信号处理: Block Pending Handler 与 SIGKILL/SIGSTOP 实验

1. 信号处理机制的 “三张表” kill -l &#xff1a;前 31 个信号为系统标准信号。 block pending handler 三张表保存在每个进程的进程控制块 —— pcb 中&#xff0c;它们分别对应了某一信号的阻塞状态、待处理状态以及处理方式。 block &#xff1a;通过 sigset_t 类型实现&…

YOLO11改进 | 检测头 | 融合渐进特征金字塔的检测头【AFPN3】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 本文介绍了一个渐进特征金字塔网络&…

关于 S7 - 1200 通过存储卡进行程序更新

西门子S7-1200系列PLC可以通过存储卡进行程序的更新&#xff0c;固件版本的升级以及程序数据的存储多项功能。本例进行程序更新的操作。 存储卡的订货号以及存储容量 一&#xff1b;如何插入存储卡 在CPU断电下&#xff0c;将CPU上挡板向下掀开&#xff0c;可以看到右上角有一…

ai写作论文会被检测吗?分享市面上7款自动写论文网站

近年来&#xff0c;随着人工智能技术的飞速发展&#xff0c;AI写作工具在学术界引起了广泛关注。然而&#xff0c;这些工具的使用也引发了关于学术诚信和检测机制的讨论。根据多所高校的声明&#xff0c;为了应对AI代写论文的现象&#xff0c;许多高校已经开始引入论文检测工具…

Python入门:深入了解__init__.py 文件(如何实现动态导入子模块)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 `__init__.py` 的作用示例:📝 如何编写 `__init__.py`1. 空的 `__init__.py`2. 导入子模块3. 初始化代码4. 动态导入子模块📝 编写 `__init__.py` 的技巧和注意事项⚓️ 相关链接 ⚓️📖 介绍 📖 在…

01:(寄存器开发)点亮一个LED灯

寄存器开发 1、单片机的简介1.1、什么是单片机1.2、F1系列内核和芯片的系统架构1.3、存储器映像1.4、什么是寄存器 2、寄存器开发模板工程3、使用寄存器点亮一个LED4、代码改进15、代码改进2 本教程使用的是STM32F103C8T6最小系统板&#xff0c;教程来源B站up“嵌入式那些事”。…

前缀和(6)_和可被k整除的子数组_蓝桥杯

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 前缀和(6)_和可被k整除的子数组 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 …

kubeadm部署k8s

1.1 安装Docker [rootk8s-all ~]# wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.huaweicloud.com/docker-ce/linux/centos/docker-ce.repo [rootk8s-all ~]# sed -i sdownload.docker.commirrors.huaweicloud.com/docker-ce /etc/yum.repos.d/docker-ce.repo [ro…

基于Keras的U-Net模型在图像分割与计数中的应用

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色&a…

Yocto - 使用Yocto开发嵌入式Linux系统_07 构建使用的临时文件夹

Detailing the Temporary Build Directory 在本章中&#xff0c;我们将尝试了解映像生成后临时构建目录的内容&#xff0c;并了解 BitBake 如何在烘焙过程中使用它。此外&#xff0c;我们还将了解这些目录中的某些内容如何在出现问题时作为有价值的信息来源来帮助我们。 In thi…

前缀和——从LeetCode题海中总结常见套路

目录 前缀和定义 截断前缀和DP&#xff1a;LeetCode53.最大子序和 经典左右指针&#xff1a;LeetCode209.长度最小的子数组 暴力求解&#xff1a;超时 优雅的双指针写法一&#xff1a; 优雅的双指针写法二&#xff1a; LeetCode.1588.所有奇数长度子数组的和 手速题&am…

springboot系列--web相关知识探索三

一、前言 web相关知识探索二中研究了请求是如何映射到具体接口&#xff08;方法&#xff09;中的&#xff0c;本次文章主要研究请求中所带的参数是如何映射到接口参数中的&#xff0c;也即请求参数如何与接口参数绑定。主要有四种、分别是注解方式、Servlet API方式、复杂参数、…

[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解

[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解 前言: 古人云: 得卧龙者&#xff0c;得天下。 然在当今大语言模型流行的时代&#xff0c;同样有一句普世之言: 会微调技术者&#xff0c;得私域大模型部署之道&#xff01; 在众多微调技术中&#xff0c;LoRA (…