《C++标准库》学习笔记 — STL — 并发 — 线程同步与并发 — mutex 与 lock

2023-11-03

一、线程同步与并发并发问题

1、出错情况

(1)未同步化的数据访问

并行运行的两个线程分别读和写同一个数据。

std:;vector<int> vec;
if (!vec.empty())
{
	std::cout << v.front() << std::endl;
}

上述代码在单线程中的运行可以满足其语义。然而,在多线程情况中,我们不能保证在 if 判断之后和 front 调用之前,该 vector 没有被改变。

(2)写至半途的数据

如果两个线程分别执行下面两段代码:

long long x = 0;
x = -1;
std::cout << x;

打印的值是不确定的,有可能是-1,有可能是0,有可能是在某次写入只写入一半时的数据。C++标准并不保证对于基本类型的写入是原子操作

(3)重新安排的语句

考虑下面三段代码(第一段为全局变量声明,剩下两段分别为两个线程中运行的代码):

long data;
bool readyFlag = false;
data = 42;
readyFlag = true;
while (!readyFlag)
{
	;
}

foo(data);

如果代码按照我们书写的顺序执行,我们确实可以认为在调用 foo() 函数时,其参数为42。然而,实际情况却是编译器或硬件可能会调整语句的执行顺序。因为C++标准只要求编译的代码在单一线程内的可观测行为正确

2、解决问题需要的特性

  • 原子性:对共享变量的访问应该是独占式,不应该被其他线程打断的。因此,线程之间不应该读取到变量的中间状态。
  • 次序:我们需要保证某些语句的执行次序。

C++标准库所提供的办法包括:

  • 使用 futurepromise。它们都保证原子性和次序:一定是在得到结果后才设定共享状态。因此,对于共享状态的读和写不会同时发生。
  • 使用 mutexlock 来处理临界区或保护区。其提供了原子性,使得我们可以实施对于相同资源的读写控制。
  • 使用 conditional variable 有效地某线程等待某个判断式称为真。
  • 使用 atomic data tyoe 确保对变量或对象的访问动作是不可切割的,只要其操作顺序是稳定的。
  • 使用 atomic data tyoe 的低层接口,它允许我们从某种程度上限制某种代码的运行次序。

3、C++并发的支持

在C++11之前,语言本身和标准库都不支持并发,虽然编译器实现可以对此提供某些支持。C++11中,不论内核语言或标准都加强支持并发编程。

对于语言本身:

  • 如今具备了这样一个内存模型,保证当你修改“被不同线程使用的”不同对象时,它们彼此独立。在C++11之前,并不保证“某线程涂写一个char”不会干涉“另一线程涂写另一个char”。
  • 引入新关键字 thread_local,用来定义线程特定的变量和对象。

标准库提供以下保证:

  • 一般而言,多个线程共享同一个程序库对象而其中至少一个线程改动该对象时,可能会导致不明确行为。特别是,当某个线程的对象构造期间,另一个线程使用该对象,会导致不明确行为。析构的情况与之类似。
  • STL容器和适配器中,并发的只读访问是允许的,并发的处理同一容器内的不同元素是可以的。
  • 对标准流进行格式化输入和输出的并发处理是可能的。虽然这可能引发插叙字符(乱序)。这项保证适用于 std::instd::outstd::err,不适用于 string streamfile streamstream buffer
  • atexit()at_quck_exit() 的并发调用是同步的。相同情况适用于 newterminateunexpected handler 的函数。此外,getenv() 的调用也是同步的。
  • 默认分配器的所有成员函数,除了析构函数,其并发处理都是同步的。

二、Mutex 和 Lock

Mutex 全名 mutual exclusion,是个 object,用来协助采取独占地排他方式控制”对资源的并发访问“。这里所谓“资源”可能是个 object,或多个 object 的组合。为了获得独占式的资源访问能力,相应的线程锁定 mutex,这样可以防止其它线程也锁定 mutex

1、使用 mutex 和 lock

下面的代码展示了在两个线程中,互斥体的使用:

// global
int val;
std::mutex valMutex;
// thread1
valMutex.lock();
if (val >= 0) {
	f(val);
} else {
	f(-val);
}
valMutex.unlock();
// thread2
valMutex.lock();
val++;
valMutex.unlock();

这里需要注意,mutex 的构造并不和某个对象绑定。其和一个或一组对象的绑定是语义上的,体现在代码中。

但是,这种使用较为麻烦。有时我们会在一个线程的多处进行退出;有时线程中会抛出异常。如果在任何一个地方我们忘记解锁,就会造成死锁的结果。因此,我们需要遵守 RAII 守则(可以参考 《Effictive C++》学习笔记 — 资源管理)。在标准库中,我们可以使用 lock_guard 实现这样的功能:

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

int val;
mutex valMutex;

int main()
{
	lock_guard lg(valMutex);
	if (val > 10)
	{
		return 1;
	}
	else if (val < 0)
	{
		return -1;
	}
	else
	{
		return 0;
	}
}

如果不使用这种方式,我们可能需要使用 goto 这种非结构化编程的方式。

2、recursive lock

有时候,递归锁定是必要的。典型例子是数据库接口,它在每个公有函数内放一个 mutex 并取得 lock,以防止数据竞争带来的对象状态异常。例如:

class DBAcess
{
private:
	mutex dbMutex;
	...
public:
	void createTable(...)
	{
		lock_guard lg(dbMutex);
		...
	}

	void insertData(...)
	{
		lock_guard lg(dbMutex);
		...
	}
};

但是如果有一个接口想要调用另外一个接口,将会造成死锁:

void createTableAndInsertTable(...)
{
	lock_guard lg(dbMutex);
	...
	createTable(...);
}

在这种情况下,我们可以使用 recursive lock。这种 mutex 允许我们多次 lock,只要 unlocklock 是一一对应的。

3、尝试性的 lock 和带时间性的 lock

有时候程序想要获得一个 lock 但如果不可能成功的话,它不想永远阻塞。针对这种情况,我们可以使用 try_lock(),它试图取得一个 lock,成功就返回 true,否则就返回 false

为了仍然能够使用 lock_guard (使当前作用域的任何出口都会自动解锁),我们可以传一个额外的实参 adopt_lock 给其构造函数,其作用在于不再构造时加锁:

while (valMutex.try_lock() == false) {
	doSomething();
}

lock_guard lg(valMutex, adopt_lock);

为了等待特定长度的时间,我们可以选用 timed_mutex

timed_mutex valMutex;

if (valMutex.try_lock_for(chrono::seconds(1)))
{
	lock_guard lg(valMutex, adopt_lock);
}

4、处理多个 lock

不同的锁可能会控制着不同的资源。但是,在某些事务中,可能我们需要按某种固定的顺序访问这些资源并在它们之间进行数据传递。因此,我们需要同时加多个锁。C++标准库中提供了全局 lock 函数来实现这种功能:

mutex mutex1;
mutex mutex2;
mutex mutex3;

lock(mutex1, mutex2, mutex3);

lock_guard lg1(mutex1, adopt_lock);
lock_guard lg2(mutex2, adopt_lock);
lock_guard lg3(mutex3, adopt_lock);

类似地,我们可以使用全局 try_lock() 函数对多个互斥体尝试加锁。该函数在所有加锁都成功的情况下会返回-1,否则返回加锁失败的 index,该次序与参数次序一致。

5、unique_lock

除了使用 lock_guard,C++标准库还提供了 unique_lock。该类除了支持 RAII,还支持我们指定何时以及如何解锁。因此,一个 unique_lock 对象可能拥有一个锁住的互斥体,也可能没有。该类提供的方法如下:
在这里插入图片描述
其功能与 unique_ptr 非常类似,允许在不同的对象之间交换所管理的 mutex 对象及状态。书中关于 release() 的说法有些问题。该函数仅仅用于解除 unique_lock 和其管理的 mutex 的关联,而不会释放 mutex

6、shared_mutex 和 shared_lock

shared_mutex 是C++17中提出的一种互斥体。这种互斥体类似读写锁,提供了两种不同级别的访问:使用 lock() 会阻止所有线程其他线程锁定互斥体;使用 shared_lock() 可以在多线程中同时访问数据:

#include <iostream>
#include <future>
#include <shared_mutex>
#include <chrono>
#include <memory>
using namespace std;

shared_mutex mutex1;
int testData = 1;

void lockData()
{
	unique_lock<shared_mutex> mutex(mutex1);
	for (int i = 0; i < 5; ++i)
	{
		this_thread::sleep_for(chrono::milliseconds(100));
		cout << "lockData -- " << testData << endl;
		testData++;
	}
}

void sharedLockReadData()
{
	shared_lock<shared_mutex> mutex(mutex1);
	for (int i = 0; i < 5; ++i)
	{
		this_thread::sleep_for(chrono::seconds(1));
		cout << "sharedLockReadData -- " << this_thread::get_id() << " --" << testData << endl;
	}
}

int main()
{
	auto result1 = async(lockData);
	auto result2 = async(sharedLockReadData);
	auto result3 = async(sharedLockReadData);

	result1.get();
	result2.get();
	result3.get();
}

在这里插入图片描述

7、只调用一次

有时候某些数据在第一次被初始化后,其他线程直接使用该数据即可。以单例的懒汉式为例。如果我们想要正确的在多线程情况下实现懒汉式,需要在判断静态对象是否为空之前增加锁。在C++中,我们可以使用 once_flagcall_once 实现此功能:

#include <iostream>
#include <future>
#include <shared_mutex>
#include <chrono>
#include <memory>
using namespace std;

class Data
{
public:
	Data()
	{
		this_thread::sleep_for(chrono::milliseconds(100));
		cout << "initData" << endl;
	}
	static Data* getData()
	{
		/*if (data == nullptr)
		{
			data = new Data;
		}*/

		call_once(initFlag, []() {data = new Data; });
		return data;
	}

private:
	static Data* data;
	static once_flag initFlag;
};

Data* Data::data = nullptr;
once_flag Data::initFlag;

void getData()
{
	Data::getData();
}

int main()
{
	auto result1 = async(getData);
	auto result2 = async(getData);

	result1.get();
	result2.get();
}

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

《C++标准库》学习笔记 — STL — 并发 — 线程同步与并发 — mutex 与 lock 的相关文章

  • 如何在MVVM中管理多个窗口

    我知道有几个与此类似的问题 但我还没有找到明确的答案 我正在尝试深入研究 MVVM 并尽可能保持纯粹 但不确定如何在坚持模式的同时启动 关闭窗口 我最初的想法是向 ViewModel 发送数据绑定命令 触发代码来启动一个新视图 然后通过 X
  • 无法使用已与其底层 RCW 分离的 COM 对象。在 oledb 中

    我收到此错误 但我不知道我做错了什么 下面的代码在backrgroundworker中 将异常详细信息复制到剪贴板 System Runtime InteropServices InvalidComObjectException 未处理 通
  • 访问私人成员[关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 通过将类的私有成员转换为 void 指针 然后转换为结构来访问类的私有成员是否合适 我认为我无权修改包含我需要访问的数据成员的类 如果不道德 我
  • 是否可以强制 XMLWriter 将元素写入单引号中?

    这是我的代码 var ptFirstName tboxFirstName Text writer WriteAttributeString first ptFirstName 请注意 即使我使用 ptFirstName 也会以双引号结束 p
  • 当我使用“control-c”关闭发送对等方的套接字时,为什么接收对等方的套接字不断接收“”

    我是套接字编程的新手 我知道使用 control c 关闭套接字是一个坏习惯 但是为什么在我使用 control c 关闭发送进程后 接收方上的套接字不断接收 在 control c 退出进程后 发送方的套接字不应该关闭吗 谢谢 我知道使用
  • 将数组向左或向右旋转一定数量的位置,复杂度为 o(n)

    我想编写一个程序 根据用户的输入 正 gt 负 include
  • 从父类调用子类方法

    a doStuff 方法是否可以在不编辑 A 类的情况下打印 B did stuff 如果是这样 我该怎么做 class Program static void Main string args A a new A B b new B a
  • 当 contains() 工作正常时,xpath 函数ends-with() 工作时出现问题

    我正在尝试获取具有以特定 id 结尾的属性的标签 like span 我想获取 id 以 国家 地区 结尾的跨度我尝试以下xpath span ends with id Country 但我得到以下异常 需要命名空间管理器或 XsltCon
  • 指针问题(仅在发布版本中)

    不确定如何描述这一点 但我在这里 由于某种原因 当尝试创建我的游戏的发布版本进行测试时 它的敌人创建方面不起作用 Enemies e level1 3 e level1 0 Enemies sdlLib 500 2 3 128 250 32
  • 从路径中获取文件夹名称

    我有一些路c server folderName1 another name something another folder 我如何从那里提取最后一个文件夹名称 我尝试了几件事 但没有成功 我只是不想寻找最后的 然后就去休息了 Thank
  • 将自定义元数据添加到 jpeg 文件

    我正在开发一个图像处理项目 C 我需要在处理完成后将自定义元数据写入 jpeg 文件 我怎样才能做到这一点 有没有可用的图书馆可以做到这一点 如果您正在谈论 EXIF 元数据 您可能需要查看exiv2 http www exiv2 org
  • 如何将单个 char 转换为 int [重复]

    这个问题在这里已经有答案了 我有一串数字 例如 123456789 我需要提取它们中的每一个以在计算中使用它们 我当然可以通过索引访问每个字符 但是如何将其转换为 int 我研究过 atoi 但它需要一个字符串作为参数 因此 我必须将每个字
  • 当操作繁忙时,表单不执行任何操作(冻结)

    我有一个使用 C 的 WinForms 应用程序 我尝试从文件中读取一些数据并将其插入数据表中 当此操作很忙时 我的表单冻结并且无法移动它 有谁知道我该如何解决这个问题 这可能是因为您在 UI 线程上执行了操作 将文件和数据库操作移至另一个
  • WCF:将随机数添加到 UsernameToken

    我正在尝试连接到用 Java 编写的 Web 服务 但有些东西我无法弄清楚 使用 WCF 和 customBinding 几乎一切似乎都很好 除了 SOAP 消息的一部分 因为它缺少 Nonce 和 Created 部分节点 显然我错过了一
  • 为什么我收到“找不到编译动态表达式所需的一种或多种类型。”?

    我有一个已更新的项目 NET 3 5 MVC v2 到 NET 4 0 MVC v3 当我尝试使用或设置时编译出现错误 ViewBag Title财产 找不到编译动态表达式所需的一种或多种类型 您是否缺少对 Microsoft CSharp
  • Process.Start 阻塞

    我正在调用 Process Start 但它会阻止当前线程 pInfo new ProcessStartInfo C Windows notepad exe Start process mProcess new Process mProce
  • mysql-connector-c++ - “get_driver_instance”不是“sql::mysql”的成员

    我是 C 的初学者 我认为学习的唯一方法就是接触一些代码 我正在尝试构建一个连接到 mysql 数据库的程序 我在 Linux 上使用 g 没有想法 我运行 make 这是我的错误 hello cpp 38 error get driver
  • 如何在 C++ BOOST 中像图形一样加载 TIFF 图像

    我想要加载一个 tiff 图像 带有带有浮点值的像素的 GEOTIFF 例如 boost C 中的图形 我是 C 的新手 我的目标是使用从源 A 到目标 B 的双向 Dijkstra 来获得更高的性能 Boost GIL load tiif
  • 使用 libcurl 检查 SFTP 站点上是否存在文件

    我使用 C 和 libcurl 进行 SFTP FTPS 传输 在上传文件之前 我需要检查文件是否存在而不实际下载它 如果该文件不存在 我会遇到以下问题 set up curlhandle for the public private ke
  • 恢复上传文件控制

    我确实阅读了以下帖子 C 暂停 恢复上传 https stackoverflow com questions 1048330 pause resume upload in c 使用 HTTP 恢复上传 https stackoverflow

随机推荐

  • lintcode 1692. 组队打怪

    你现在有n个英雄 每个英雄的战斗力为atk1 你要用这些英雄去对付n个怪物 每个怪物的战斗力为atk2 在一场战斗中 你需要安排每个英雄分别与一个怪兽战斗 如果英雄战斗力高于怪兽 那个怪兽就会被击杀 问最多能击杀几个怪兽 给定atk1 6
  • excel二进制移位运算_Excel揭秘13:在Excel中实现位运算

    我们知道 计算机使用的是二进制计数法 也就是说 在计算机中的所有信息都是使用二进制来存储和处理的 下表列出了我们熟悉的十进制数及与其相对应的二进制数 位运算规则 在位运算中 按位与 运算 AND运算 分别按位比较两个相应的数字 0或1 当且
  • centos7搭建svn服务器

    一 安装svn服务器 root svnserver yum y install subversion 查看svn 安装位置 可以用以下命令 root svnserver rpm ql subversion etc subversion et
  • Java中String类的使用

    目录 1 MS String 类中两种对象实例化的区别 1 1 直接赋值 1 2 构造方法 2 字符 字节与字符串 2 1 字符与字符串转换 2 2 字节与字符串转换 3 字符串常见操作 3 1 字符串比较 3 2 字符串查找 3 3 字符
  • HS BDC 【HDU - 3472】【混合半欧拉图构建欧拉图+最大流】

    题目链接 有N个字符 如果字符可以首尾相同字符相接组成一条链的话 那么就是说明是well done的 不然 就不是 所以考虑成一条边 我们把每个字符串考虑成有向边 又有些字符串是可以反转的 实际上可以把它当成是无向边来考虑 现在 就是要知道
  • idea 生成项目结构图

    Terminal中输入tree D mybatis plus generator demo gt tree 文件夹 PATH 列表 卷序列号为 ECE0 24D1 D idea inspectionProfiles libraries mv
  • Java如何将一个对象的所有字段值赋值给另一个对象?

    我们开发的时候可能需要进行对象值的复用 下面给大家介绍一个方法 就是使用BeanUtils public static void main String args throws Exception Student student new S
  • MySQL 经典练习 50 题(完美解答版)

    一 创建数据库和表 数据库 学生表 student 课程表 course 教师表 teacher 成绩表 score 表关系 创建数据库和表 创建数据库 drop database if exists mysql test cascade
  • 51单片机EEPROM(I²C总线通信)AT24C02数据存储

    一 存储器介绍 补充 1 易失性存储器 RAM 存储速度特别快但掉电丢失 SRAM 运行速度最快 用于电脑CPU 高速缓存 单片机中的SRAM 定义一个变量就会存在SRAM中 使用触发器做的 存储容量小 成本高 DRAM 运行速度仅次于SR
  • Linux的GPIO子系统解析 ( 一 ) 之 gpiolib.c

    文章目录 Linux的GPIO子系统解析 一 之 gpiolib c 绪论 关于GPIO子系统库文件的gpiolib c解析 drivers gpio gpiolib c gpio desc结构体 gpio chip结构体 gpio ens
  • ModbusSlave安装及使用指南正式版带序列码

    ModbusSlave是一个从站设备仿真软件 它用于接收主设备的命令包 并回送数据包 可用于测试和调试Modbus主站设备 便于观察Modbus通信过程中的各种报文 ModbusSlave支持ModbusRTU ASCII TCP IP等协
  • 基于Xposed hook 实时监测微信消息的三种策略

    本文以微信版本6 7 3为例进行分析有hook 大部分做微信机器人的话 首先要实时抓取微信的消息 在这里展示三种方式对微信的消息进行hook 1 基于UI层拉取加载进行监听 2 基于微信dao层调用的保存进行监听 3 基于数据库的插入保存进
  • 决策树 prepruning_数据挖掘入门系列教程(三点五)之决策树

    本来还是想像以前一样 继续学习 Python数据挖掘入门与实践 的第三章 决策树 但是这本书上来就直接给我怼了一大串代码 对于决策树基本上没有什么介绍 可直接把我给弄懵逼了 主要我只听过决策树还没有认真的了解过它 这一章节主要是对决策树做一
  • SD方法

    4 4 SD方法 结构化的设计方法 数据流图 gt 软件结构图 描述软件的结构图 层次图 结构图 SD方法 DFD gt 软件结构图 1 DFD图的类型 变换型 为核心处理准备数据 输入数据流 核心处理 变换中心 对核心处理的结果作出处理
  • CAP和BASE理论

    CAP理论 CAP是 Consistency Availability Partition tolerance 三个词语的缩写 分别表示一致性 可用性 分区容忍性 它指出一个分布式计算系统不可能同时满足以下三点 一致性 Consistenc
  • 小白学PYTHON时最容易犯的6个错误,看看你遇到过几个

    最近又在跟之前的同学一起学习python 一起进步 发现很多测试同学在初学python的时候很容易犯一些错误 特意总结了一下 其实这些错误不仅是在学python时会碰到 在学习其他语言的时候也同样会碰到 错误1 缩进 python是强制缩进
  • GET 和 POST的区别? 用POST方法发送登陆请求

    GET 和 POST的区别 用POST方法发送登陆请求 lt 1 gt http方法 http协议定义了很多方法对应不同的资源操作 其中最常用的是GET 和 POST 方法 GET POST OPTIONS HEAD PUT DELETE
  • 什么是计算机网络中的127.0.0.1 IP地址或Localhost?

    IP addresses are used to specify the hosts in a numeric way There are different types of IP addresses in Computer networ
  • 听说渲影很便宜,是真的吗?

    这次我比较了3个平台 炫云 渲影和渲染100 首先说结论 渲影是很便宜 但也没便宜过渲染100 而且出图大小有猫腻 具体的往下看 首先我选取了一个219M的场景 不是很大 设置的分辨率是3200 4000 提交3个平台的时候选择的参数也一样
  • 《C++标准库》学习笔记 — STL — 并发 — 线程同步与并发 — mutex 与 lock

    C 标准库 学习笔记 STL 并发 线程同步与并发 mutex 与 lock 一 线程同步与并发并发问题 1 出错情况 1 未同步化的数据访问 2 写至半途的数据 3 重新安排的语句 2 解决问题需要的特性 3 C 并发的支持 二 Mute