并发线程 (2) - C++线程间共享数据【详解:如何使用锁操作】

news2024/11/26 15:33:38

系列文章目录

C++技能系列
Linux通信架构系列
C++高性能优化编程系列
深入理解软件架构设计系列
高级C++并发线程编程

期待你的关注哦!!!
在这里插入图片描述

快乐在于态度,成功在于细节,命运在于习惯。
Happiness lies in the attitude, success lies in details, fate is a habit.

线程间共享数据

  • 系列文章目录
  • 1、但是如何防止恶性的条件竞争呢?
  • 2、如何用互斥保护共享数据?
    • 2.1 如何使用互斥?
    • 2.2 本意互斥保护数据却留有余地,如何防止隐患呢?
    • 2.3 如何解决容器本身接口固有的条件竞争?
    • 2.4 如何解决死锁问题?
    • 2.5 如何使用std::unique_lock转移互斥归属权?
    • 2.6 如何使用std::unique_lock按合适的粒度加锁?
    • 2.7 如果对很少更新的数据结构该如何优化加锁?
    • 2.8 如何递归加锁?
  • 3、多线程中如何在初始化过程中保护共享数据
  • 4、小结

具体哪个线程按何种方式访问什么数据?还有,一旦改动了数据,如果牵涉到其他线程,它们要在何时以什么通信方式获得通知?同一进程内的多个线程之间,虽然可以简单易行地共享数据,但这不是绝对的优势,优势甚至是很大的劣势。不正确使用共享数据,是产生与开发有关的错误的一个很大的诱因。

如果共享数据都是只读数据,就不会有问题;但是,同时一旦有删除删除数据就会出现问题。

在并发编程中,操作由两个或者多个线程负责,它们争先让线程执行各自的操作,而结果取决于它们执行的相对次序,所有这种情况都是条件竞争

1、但是如何防止恶性的条件竞争呢?

  • 有锁数据结构:采取保护措施包装数据结构,确保不变量被破坏时,中间状态只对执行改动线程可见。在其他访问同一数据结构的视角中,这种改动要么尚未开始,要么已经完成。
  • 无锁数据结构:修改数据结构的设计及其不变量,由一连串不可拆分的改动完成数据变更,每个改动都维持数据变量不被破坏。通常这种编程难以正确编写。

保护共享数据的最基本方式就是互斥

2、如何用互斥保护共享数据?

访问一个数据结构前,先锁住与数据相关的互斥;访问结束后,再解锁互斥。C++线程库保证了,一旦有线程锁住了某个互斥,若其他线程试图再给它加锁,则须等待,直至最初成功加锁的线程把该互斥解锁。这确保了全部线程所见到的共享数据是正确的,不变量没有没有被破坏。

2.1 如何使用互斥?

通过 std::mutex的实例创建互斥,调用成员函数lock()对其加锁,调用unlock()解锁。但是一般不推荐直接调用成员函数的做法。原因是若按此处理,那就必须记住,在函数以外的每条代码路径上都要调用unlock(),包括由于异常导致退出的路径。

C++标准库提供了类模版std::lock_guard<>,针对互斥融合实现了RAII机制:在构造是给互斥加锁,在析构时给互斥解锁,从而保证互斥总被正确的解锁。

如下,用互斥保护链表:

#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list;
void add_to_list(int new_value)
{
	std::lock_guard<std::mutex> guard(some_mutex);
	some_list.push_back(new_value);
}
bool list_contains(int value_to_find)
{
	std::lock_gurd<std::mutex> guard(some_mutex);
	return std::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}

2.2 本意互斥保护数据却留有余地,如何防止隐患呢?

如果成员函数返回的指针或者引用,指向受保护的共享数据,那么即便成员函数全都良好、有序的方式锁定互斥,仍然无济于事,因为受保护已被打破,出现大漏洞。 只要存在任何能访问该指针和引用的代码,它就可以访问受保护的共享数据,则须谨慎设计程序接口,从而确保互斥先行锁定,再对受保护的共享数据进行访问,并保证不留后门。

我们来看看,意外的向外传递引用,指向受保护的共享数据:

class some_data
{
	int a;
	std::string b;
public:
	void do_something();
};
class data_wrapper
{
private:
	some_data data;
	std::mutex m;
public:
	template<typename Fubction>
	void process_data(Function func)
	{
		std::lock_guard<std::mutex> l(m);
		//向使用者提供的函数传递受保护的数据
		func(data);
	}
};
some_data* unprotected;
void malicious_function(some_data& protected_data)
{
	unprotected = &protected_data;
}
data_wrapper x;
void foo()
{
	//传入恶意函数
	x.process_data(malicious_function);
	//以无保护方式访问本应受保护的共享数据
	unprotected->do_something();
}

我们除了要检查成员函数,防止向调用者传出指针或引用,还必须检查另一种情况:若成员函数在自身内部调用了别的函数,而这些函数却不受我们掌控,那么,也不得向他们传递这些指针或引用。如果有就很危险。

2.3 如何解决容器本身接口固有的条件竞争?

多线程访问的情况下STL容器内的empty()size()的结果不可信。尽管,在某个线程调用empty()size()时,返回值可能是正确的。然而,一旦函数返回,其他线程就不再受限,从而能自由地访问栈容器,可能马上有新元素入栈,或者,现有的元素会立刻出栈,令前面的线程得到结果失效而无法使用。

在空栈上调用top()会导致未定义行为。

但是如何解决上述问题?

  • 传入引用;
  • 提供不抛出异常的拷贝构造函数或不抛出异常的移动构造函数;
  • 返回指针指向弹出的元素。

如下代码,线程安全的栈容器类:

#include <exception>
#include <memory>
#include <mutex>
#include <stack>
struct empty_stack: std::exception
{
	const char* what() const throw();
};
template<typename T>
class threadsafe_stack
{
private:
	std::statck<T> data;
	mutable std::mutex m;
public:
	threadsafe_stack(){}
	threadsafe_stack(const threadsafe_stack& other)
	{
		std::lock_guard<std::metux> lock(other.m);
		//在构造函数的函数体(constructor body)内进行复制操作
		data = other.data;
	}
	//将赋值运算符删除
	threadsafe_stack& operator=(const threadsafe_stack&) = delete;
	void push(T new_value)
	{
		std::lock_guard<std::mutex> lock(m);
		data.push(std::move(new_value));
	}
	//返回std::share_ptr<T>
	std::share_ptr<T> pop()
	{
		std::lock_guard<std::mutex> lock(m);
		//试图在弹出前检查是否为空栈
		if(data.empty()) throw empty_stack();
		//改动栈容器前设置返回值
		std::share_ptr<T> const res(std::make_shared<T>(data.top()));
		data.pop();
		return res;
	}
	//接收引用参数,指向某外部变量的地址,存储弹出的值
	void pop(T& value)
	{
		std::lock_guard<std::mutex> lock(m);
		if(data.empty()) throw empty_stack();
		value = data.top();
		data.pop();
	}
	bool empty() const
	{
		std::lock_guard<std::mutex> lock(m);
		return data.empty();
	}
};

2.4 如何解决死锁问题?

线程在互斥上争夺抢锁:有两个线程,都需同时锁住两个互斥,都等待着再给另一个互斥加锁。于是,双方毫无进展,因为它们同时在苦苦等待对方解锁互斥。此时造成死锁。
死锁:两个线程互相等待,停滞不前。

防范死锁的建议通常是始终按相同的顺序对两个互斥加锁。若我们总是先锁互斥A,在锁互斥B,则永远不会发生死锁。

(1)运用std::lock函数和std::lock_guard类模版,进行内部数据的互换操作:

class some_big_object;
void swap(some_big_object& lhs, some_big_object& rhs);
class X
{
private:
	some_big_object some_detail;
	std::mutex m;
public:
	X(some_big_object const& sd):some_detail(sd){}
	friend void swap(X& lhs, X& rhs)
	{
		if(&lhs == &rhs)
			return;
		std::lock(lhs.m, rhs.m);
		//std::adopt_lock指明互斥上已被锁住,即互斥上有锁存在
		std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
		std::lock_guard<std::mutex> lock_a(rhs.m, std::adopt_lock);
		swap(lhs.some_detail, rhs.some_detail);
    }
}

std::adopt_lock指明互斥上已被锁住,即互斥上有锁存在

(2)使用std::unique_lock锁,如下代码:

//实例std::defer_lock将互斥保留为无锁状态
std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
std::unique_lock<std::mutex> lock_b(lhs.m, std::defer_lock);
//到这里才对互斥加锁
std::lock(lock_a, lock_b);

std::unique_lock占用更多的空间,也比std::lock_guard更慢,但是std::unique_lock对象可以不占用关联的互斥,具备这份灵活性需要付出代价:需要存储并且更新互斥信息。
std::defer_lock将互斥保留为无锁状态

(3)C++17 提供一个新的RAII类模版std::scoped_lock<>。std::scoped_lock<>和std::scoped_guard<> 完全等价,只不过前者是可变参数类模版,接收各种互斥类型作为模版参数类表,还以多个互斥对象作为构造函数的参数列表。

swap(X& lhs, X& rhs)
{
	if(&lhs == &rhs)
		return;
	std::scoped_lock guard(lhs.m, rhs.m);

	swap(lhs.some_detail, rhs.some_detail);	
}

std::scoped_lock guard(lhs.m, rhs.m)等价于 std::scoped_lock<std::mutex, std::mutex> guard(lhs.m, rhs.m);

2.5 如何使用std::unique_lock转移互斥归属权?

因为std::unique_lock实例不占有与之关联的互斥,所以随着其实例的转移,互斥的归属权可以在多个std::unique_lock实例之间转移。std::unique_lock是可移动不可复制

转移有一种用途:准许函数锁定互斥,然后把互斥的归属权转移给函数调用者,好让它在同一锁的保护下执行其他操作

看如下代码,get_lock() 函数先锁定互斥,接着对数据做前期准备,再将归属权返回给调用者:

std::unique_lock<std::mutex> get_lock()
{
	extern std::mutex some_mutex;
	std::unique_lock<std::mutex> lk(some_mutex);
	prepare_data();
	return lk;
}
void process_data()
{
	std::unique_lock<std::mutex> lk(get_lock());
	do_something();
}

由于锁lk是get_lock函数中声明的std::unique_lock局部变量,因此代码无需调用std::move()就能把它直接返回,编译器会妥善调用移动构造函数。

2.6 如何使用std::unique_lock按合适的粒度加锁?

粒度精细的锁保护少量数据,而粒度粗大的锁保护大量数据。锁操作有两个要点:

  • 选择足够粗大的锁粒度,确保目标数据受到保护;
  • 限制范围,务求只在必要的操作过程中持锁。

std::unique_lock类具有成员函数lock()unlock()try_lock()
可用std::unique_lock处理:假如代码不再需要访问共享数据,那么我们就调用unlock()解锁;若以后需要重新访问,则调用lock()加锁。

void get_and_process_data()
{
	std::unique_lock<std::mutex> my_lock(the_mutex);
	some_class data_to_process = get_next_data_chunk();
	my_lock.unlock();
	//假定调用process()期间,互斥无须加锁
	result_type result = process(data_to_process);
	my_lock.lock();
	//重新锁住互斥,以写出结果
	write_result(data_to_process, result);
}

若只用单独一个互斥保护整个数据结构,不但可以加剧锁的争夺,还将难以缩短缩短持锁时间。假设某种操作需对同一个互斥全程加锁,当中步骤越多,则持锁时间越久。这是一种双重损失,恰恰加倍促使我们尽可能该用粒度精细的锁。

比如删除某数据,得先进行查询再删除,可以先获取拷贝对象数据,遍历完再进行删除。减少锁持续时间。

2.7 如果对很少更新的数据结构该如何优化加锁?

采用std::mutex保护数据结构过于严苛,原因是即便没发生改动,他照样会禁止并发访问。C++17标准库提供std::share_mutex,我们需要采用新类型的互斥。由于新类型的互斥具有两种不同的使用方式,因此通常被称为读写互斥:允许单独一个“写线程”进行完全排它访问,也允许多个“读线程”共享数据或并发访问。

共享锁即读锁,对应std::shared_lock<std::shared_mutex>
排他锁即写锁,对应std::lock_guard<std::shared_mutex>和std::unique_guard<std::shared_mutex>

运用 std::shared_mutex 保护数据结构,代码如下:

#include <map>
#include <string>
#include <mutex>
#include <shared_mutex>
class dns_enty;
class dns_cache
{
	std::map<std::string, dns_entry> entries;
	mutable std::shared_mutex entry_mutex;
public:
	dns_entry find_entry(std::string const& domain) const
	{
		std::shared_lock<std::shared_mutex> lk(entry_mytex);
		std::map<std::string, dns_entry>::const_iterator const it = entries.find(domain);
		return(it == entries.end()) ? dns_entry() : it->second;
	}
	void update_or_add_entry(std::string const& domain, dns_entry const& dns_details)
	{
		std::lock_guard<std::shared_mutex> lk(entry_mutex);
		entries[domain] = dns_details;
    }
	
};

2.8 如何递归加锁?

使用std::mutex,再次对其重新加锁就会出错,将导致未定义行为。
使用std::recursive_mutex允许同一线程对某互斥的统一实例多次加锁,我们必须先释放全部的锁,才可以让另一个线程锁住该互斥。

例如:
若我们对它调用3次lock(),就必须调用3次unlock()。只要正确的使用std::lock_guard<std::recurive_mutex> std::unique_guard<std::recurive_mutex>,它们便会处理好递归锁。

3、多线程中如何在初始化过程中保护共享数据

用互斥实现线程安全的延迟初始化,代码如下:

std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;
void foo()
{
	//此处,全部线程都被迫循环运行
	std::unique_lock<std::mutex> lk(resource_mutex);
	if(resource_ptr)
	{
		//仅有初始化需要保护数据
		resource_ptr.reset(new some_resource);
	}
	lk.unlock();
	resource_ptr->do_something();
}

不过数据为多线程使用,那么它们便无法并发访问,线程只能毫无必要的运行,因为每个线程都必须在互斥上轮候,等待查验数据是否已经完成初始化。

为此,C++标准库中提供了std::once_flag类和std::call_once()函数。
令所有线程共同调用std::call_once函数,从而确保在该调用返回时,指针初始化由其中某线程安全且唯一完成(通过适合的同步机制)。必要同步数据则有std::once_flag实例存储,每个std::call_flag实例对应一次不同的初始化。相比显示使用互斥,std::call_once()函数的额外开销往往更低,特别是在初始已经完成的情况下,所以如果功能符合需求就应优先使用。

延迟初始化代码如下:

std::shared_ptr<some_resource> resource_ptr;
//实例存储
std::once_flag resource_flag;
void init_resource()
{
	resource_ptr.reset(new some_resource);
}
void foo()
{
	//初始化函数准确地被唯一一次调用
	std::call_once(resource_flag, init_resource);
	resource_ptr->do_something();
}

利用std::call_once()函数对类X的数据成员实施线程安全的延迟初始化:

class X
{
private:
	connection_info connection_details;
	connection_handle connection;
	std::once_flag connection_init_flag;
	void open_connection()
	{
		connection = connection_manager.open(connection_details);
    }
public:
	X(connection_info const& connection_details_);
		connection_details(connection_details_)
		{}
	void send_data(data_packet const& data)
	{
		std::call_once(connection_init_flag, &X::open_connection, this);
		connection.send_data(data);
	}
	data_packet receive_data()
	{
		std::call_once(connection_init_flag, &X::open_connection, this);
		return connection.receive_data();
	}
};

某些类的代码只需用到唯一一个全局实例,这种情形可用以下方法代替std::call_once():

class my_class;
//线程安全的初始化,C++11标准保证其正确性
my_class& get_my_class_instance()
{
	static my_class inctance;
	return instance;
}

多个线程可以安全地调用get_my_class_instance(),而无需担忧初始化的条件竞争。

4、小结

世上无难事,只怕有心人。

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

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

相关文章

【机器学习】——神经网络与深度学习

目录 引入 一、神经网络及其主要算法 1、前馈神经网络 2、感知器 3、三层前馈网络&#xff08;多层感知器MLP&#xff09; 4、反向传播算法 二、深度学习 1、自编码算法AutorEncoder 2、自组织编码深度网络 ①栈式AutorEncoder自动编码器 ②Sparse Coding稀疏编码 …

(一)OC对象本质---内存布局

Apple OSS Distributions GitHubApple Open Source 开源源码链接 面试题1 一个NSObject对象占用多少内存&#xff1f; 系统分配了16个字节给NSObject对象&#xff08;通过malloc_size函数获得&#xff09; ​​​​​​​但NSObject对象内部只使用了8个字节的空间&#xf…

【状态估计】粒子滤波器、Σ点滤波器和扩展/线性卡尔曼滤波器研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

推荐一些简单却很实用的工具,快收藏起来吧

第一个工具&#xff1a;remove.bg 这是一个在线抠图的神器&#xff0c;它能够帮助你轻松地消除图片中的背景。相信很多人都知道&#xff0c;手动抠图真的很累&#xff0c;抠着抠着就会觉得烦躁。但是&#xff0c;使用这个神器&#xff0c;你只需要点击上传图片&#xff0c;就能…

Git安装与使用方法入门

目录 Git简介 Git下载与安装 Git配置环境变量 Git使用方法入门 Git简介 Git是一个帮助开发者追踪代码变化和团队协作的工具。它记录了代码修改的历史&#xff0c;并允许回到过去的版本。开发者可以创建分支来独立开发新功能&#xff0c;而不影响主代码。团队成员可以共享代…

@EnableScheduling和@Scheduled注解详解fixedrate和fixeddelay的区别

一、pom.xml中导入必要的依赖&#xff1a; <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.1.RELEASE</version></parent><dependencies><…

Selenium教程__元素定位(2)

Selenium操作页面上的文本输入框、按钮、单选框、复选框等&#xff0c;凡是能在页面显示的任何元素都需要先对元素进行定位。 Selenium提供了以下方法来定位页面中元素&#xff1a; find_element_by_id&#xff1a;通过id属性值进行匹配查找&#xff0c;返回匹配到的第一个元…

利用zOffice SDK实现合同续签系统

经过用户调研和实际考察发现。商务、政务和个人的真实使用场景中&#xff0c;很多用户会有通过“用户数据”“固定模板”生成“批量合同&#xff08;文件&#xff09;”的需求&#xff0c;并且存在着使用痛点。在在线办公不断发展的今天&#xff0c;我们需要一个在线编辑的工具…

使用Jsoup工具解析页面数据

前提是需要联网 F12打开浏览器控制台&#xff0c;通过元素找到需要爬取的数据 1、添加网页解析依赖 <!--解析网页依赖--> <dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.10.2</version&g…

【id:21】【1分】E. DS单链表--类实现

题目描述 用C语言和类实现单链表&#xff0c;含头结点 属性包括&#xff1a;data数据域、next指针域 操作包括&#xff1a;插入、删除、查找 注意&#xff1a;单链表不是数组&#xff0c;所以位置从1开始对应首结点&#xff0c;头结点不放数据 类定义参考 输入 n 第1行先输…

GRE over IPsec VPN配置

GRE over IPsec VPN配置 【实验目的】 理解GRE Tunnel的概念。理解GRE over IPsec VPN的概念。掌握GRE Tunnel的配置。掌握GRE over IPsec VPN的配置。验证配置。 【实验拓扑】 实验拓扑如下图所示。 实验拓扑 设备参数表如下表所示。 设备参数表 设备 接口 IP地址 子网…

Ziya:一个自回归、双语、开源和多功能的大语言模型

什么是Ziya&#xff1f; Ziya是一个基于LLaMa的130亿参数的中英双语预训练语言模型&#xff0c;它由IDEA研究院认知计算与自然语言研究中心&#xff08;CCNL&#xff09;推出&#xff0c;是开源通用大模型系列的一员。Ziya具备翻译&#xff0c;编程&#xff0c;文本分类&#…

JS中遍历对象的方法讲解

文章目录 for...in循环当使用for...in循环遍历对象时&#xff0c;需要注意以下几点&#xff1a; Object.keys()方法结合forEach()循环Object.entries()结合forEach()循环Object.getOwnPropertyNames()方法结合forEach()循环 在JavaScript中&#xff0c;有几种常用的方法可以用来…

runjs在vue2项目中的使用

安装run.js插件 安装chalk const { run } require(runjs) const chalk require(chalk) const config require(../vue.config.js) const rawArgv process.argv.slice(2) const args rawArgv.join( )if (process.env.npm_config_preview || rawArgv.includes(--preview)) …

【科普】Windows10如何关闭搜索功能中的广告? Windows10如何关闭自动更新?

目录 一、Windows10如何关闭搜索功能中的广告&#xff1f;1.1 问题描述1.2 关闭步骤1.2.1 关闭显示搜索1.2.2 修改注册表 二、Windows10如何关闭自动更新&#xff1f;2.1 问题描述2.2 关闭步骤 一、Windows10如何关闭搜索功能中的广告&#xff1f; 1.1 问题描述 windows10的搜…

云安全技术(五)之评估云服务供商

评估云服务提供商 Evaluate Cloud Service Providers 1.1 根据标准认证 Verification against criteria ISO/EC 27001和27001:2013NIST SP 800-53支付卡行业数据安全标准(PCI DSS)SOC 1、SOC 2和SOC 3通用准则(Common Criteria)FIPS 140-2 1.2 系统/子系统产品认证 System/su…

pytest - 使用pytest过程中的5大超级技巧(实例详解篇)

从简单的断言和测试用例组织到更先进的参数化和夹具管理&#xff0c;pytest提供了强大的功能和灵活性。让我们一起探索这些技巧&#xff0c;使你的测试变得更加高效精准&#xff01; 无需担心阅读时间过长&#xff0c;本文已经为您准备了详尽的解析和实际示例。立即开始&#…

基于MATLAB的前景检测器实现道路车辆实时检测跟踪(完整代码分享)

交通问题越来越开始影响着人们的生产和生活,由于汽车拥有量的急剧增加,城市交通问题日益严重,因此交通问题开始成为人们关心的社会热点。在我国,近年来,交通事故频繁发生,有效的交通监测和管理已迫在眉睫。 完整代码: clc; clear; close all; warning off; addpath(gen…

redis源码之:字典dict

先来看看dict的大致结构&#xff1a; debug所用demo如下&#xff1a; void testDict(); int main(int argc, char **argv) {testDict(); } void testDict(){dict *dict0 dictCreate(&hashDictType, NULL);//注意key要用sds,如果是普通字符串&#xff0c;长度会判为0&…

这年头不会还有人纯文字聊天吧 ?教你用Python一键获取斗图表情包

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 很多兄弟在聊天上没有下太多的功夫&#xff0c;导致自己聊天的时候很容易尬住&#xff0c; 然后就不知道聊啥了&#xff0c;这时候合适表情包分分钟就能救场&#xff0c; 但是一看自己收藏的表情包&#xff0c;好家伙…