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

news/2024/5/19 5:54:38 标签: c++, 开发语言, 后端, 并发, mutex

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

  • 一、线程同步与并发并发问题
    • 1、出错情况
      • (1)未同步化的数据访问
      • (2)写至半途的数据
      • (3)重新安排的语句
    • 2、解决问题需要的特性
    • 3、C++并发的支持
  • 二、Mutex 和 Lock
    • 1、使用 mutex 和 lock
    • 2、recursive lock
    • 3、尝试性的 lock 和带时间性的 lock
    • 4、处理多个 lock
    • 5、unique_lock
    • 6、shared_mutex 和 shared_lock
    • 7、只调用一次

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

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

mutex__lock_67">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

mutex__shared_lock_190">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();
}

在这里插入图片描述


http://www.niftyadmin.cn/n/1340308.html

相关文章

Trie树的分析与实现

字典树 又称单词查找树&#xff0c;Trie树&#xff0c;是一种树形结构&#xff0c;是一种哈希树的变种。典型应用是用于统计&#xff0c;排序和保存大量的字符串&#xff08;但不仅限于字符串&#xff09;&#xff0c;所以经常被搜索引擎系统用于文本词频统计。它的优点是&…

iOS 元件组件-创建静态库static library

概述 在项目开发的过程中&#xff0c;经常使用静态库文件。例如两个公司之间业务交流&#xff0c;不可能把源代码都发送给另一个公司&#xff0c;这时候将私密内容打包成静态库&#xff0c;别人只能调用接口&#xff0c;而不能知道其中实现的细节。 库是一些没有main函数的程序…

《C++标准库》学习笔记 — STL — 并发 — 线程同步与并发 — 条件变量与原子操作

《C标准库》学习笔记 — STL — 并发 — 线程同步与并发 — 条件变量与原子操作一、条件变量1、意图2、条件变量的基本使用3、使用条件变量实现消费队列4、细究条件变量二、Atmoic1、使用 Atmoic2、与其他变量共同使用3、细究 Atomic 高级接口&#xff08;1&#xff09;构造&am…

Codeforces Round #326 (Div. 2) C. Duff and Weight Lifting 水题

C. Duff and Weight Lifting Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/588/problem/C Description Recently, Duff has been practicing weight lifting. As a hard practice, Malek gave her a task. He gave her a sequence of wei…

八数码问题求解(1)深度优先搜索

人工智能课&#xff0c;第一个实验就是八数码问题。老师让用3中方式都实现一遍&#xff0c;分别是广度优先搜索、深度优先搜索和启发式搜索。心塞╮(╯▽╰)╭。紧急补了一些数据结构的知识&#xff0c;就匆匆上阵。先分享深度优先搜索&#xff0c;后两篇我会分享广度优先搜索和…

《设计模式》— 结构型模式 — 组合模式

《设计模式》— 结构型模式 — 组合模式一、问题来源二、适用性三、结构四、参与者1、Component(Graphic)2、Leaf3、Composite4、Client五、协作六、效果七、实现1、显式的父部件引用2、共享组件3、最大化Component接口4、声明管理子部件的操作5、Component 是否应该实现一个 C…

基于HTML5 WebGL实现3D飞机叶轮旋转

在上一篇《基于HT for Web矢量实现2D叶轮旋转》中讲述了叶轮旋转在2D拓扑上的应用&#xff0c;今天我们就来讲讲叶轮旋转在3D上的应用。 在3D拓扑上可以创建各种各样的图元&#xff0c;在HT for Web系统中提供了一些常规的3D模型&#xff0c;但是对于那些比较复杂的模型&#x…

《设计模式》— 结构型模式 — 装饰模式(包装器)

《设计模式》— 结构型模式 — 装饰模式&#xff08;包装器&#xff09;一、动机二、适用性三、结构四、参与者1、Component2、ConcreteComponent3、Decorator4、ConcreteDecorator五、协作六、效果1、比静态继承更灵活2、避免在层次结构高层的类有太多的特征3、Decorator 与它…