《C++并发编程实战》读书笔记(2):线程间共享数据
1、使用互斥量
在C++中,我们通过构造std::mutex的实例来创建互斥量,调用成员函数lock()对其加锁,调用unlock()解锁。但通常更推荐的做法是使用标准库提供的类模板std::lock_guard<>,它针对互斥量实现了RAII手法:在构造时给互斥量加锁,析构时解锁。两个类都在头文件<mutex>里声明。
std::list<int> some_list;
std::mutex some_mutex;
void add_to_list(int value)
{
//C++17引入了类模板参数推导的新特性,所以下面语句也可以简化成:std::lock_guard guard(some_mutex);
std::lock_guard<std::mutex> guard(some_mutex);
some_list.push_back(value);
}
bool list_contains(int value)
{
std::lock_guard<std::mutex> guard(some_mutex);
return std::find(some_list.begin(), some_list.end(), value) != some_list.end();
}
2、防范死锁
假设有两个线程,都需要同时锁住两个互斥量才能进行某种操作,但它们分别只锁住了一个互斥量,都等着再给另一个互斥量加锁,这就构成了死锁。标准库提供了std::lock函数来解决死锁的问题,它可以同时锁住多个互斥量。
class some_big_object {};
void swap(some_big_object& lhs, some_big_object& rhs) {}
class X
{
private:
some_big_object some_detail;
mutable std::mutex m;
public:
X(const some_big_object& sd) :some_detail(sd) {}
friend void swap(X& lhs, X& rhs)
{
if (&lhs == &rhs) { return; }
std::lock(lhs.m, rhs.m);
std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);
swap(lhs.some_detail, rhs.some_detail);
}
};
本例中必须要判断两个参数是否指向不同的实例,因为如果已经在某个std::mutex对象上加锁,那么再次试图加锁将导致未定义的行为。构造std::lock_guard对象时,额外参数std::adopt_lock指明互斥量已被锁住,std::lock_guard实例应当据此接收锁的归属权,不得在构造函数内试图另行加锁。
针对上述场景,C++17还提供了新的RAII模板类std::scoped_lock<>,它和std::lock_guard<>完全等价,只不过前者是可变参数模板,接收各种互斥量型别作为模板参数列表,还以多个互斥量对象作为构造函数参数列表。下列代码中,传入构造函数的两个互斥量都被加锁,机制与std::lock()函数相同,因此,当构造函数完成时它们都被锁定,而后在析构函数内一起被解锁。
void swap(X& lhs, X& rhs)
{
if (&lhs == &rhs) { return; }
//这里使用了C++17的类模板参数推导特性,下面的语句完全等价于std::scoped_lock<std::mutex, std::mutex> guard(lhs.m, rhs.m);
std::scoped_lock guard(lhs.m, rhs.m);
swap(lhs.some_detail, rhs.some_detail);
}
标准库也提供了std::unique_lock<>模板,它与std::lock_guard<>一样,也是一个以互斥量作为参数的类模板,并且以RAII手法管理锁,不过它更灵活一些(代价是略微损失性能)。std::unique_lock<>的构造函数接收第二个参数,我们可以传入std::adopt_lock以指明std::unique_lock对象管理互斥量上的锁,也可以传入std::defer_lock使互斥量在完成构造时处于无锁状态,等以后有需要时再加锁。
void swap(X& lhs, X& rhs)
{
if (&lhs == &rhs) { return; }
std::unique_lock<std::mutex> lock_a(lhs.m, std::defer_lock);
std::unique_lock<std::mutex> lock_b(rhs.m, std::defer_lock);
std::lock(lock_a, lock_b);
swap(lhs.some_detail, rhs.some_detail);
}
std::unique_lock类十分灵活,它具有成员函数lock()、try_lock()、unlock(),这与互斥量的基本成员函数一致,所以该类可以结合泛型函数来使用,例如std::lock()。std::unique_lock的实例可以在销毁前通过成员函数unlock()解锁,这意味着如果执行流程的任何特定分支没有必要继续持有锁,那我们就可以提前解锁,这在有些情况下可能有助于提升程序性能。
锁的归属权可以在多个std::unique_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();
}
3、保护共享数据的其它工具
3.1、保护共享数据的初始化
假设共享数据只在初始化过程中需要保护,此后无需再进行显式的同步操作,那么可以使用std::once_flag类和std::call_once函数来处理这种情况,它们可以保证初始化操作只会执行一次。std::once_flag的实例既不可复制也不可移动,这与std::mutex类似。
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();
}
C++11规定了局部静态变量的初始化只会在某个单一线程上发生,在初始化完成之前,其它线程不会越过静态数据的声明而继续运行。如果某些类只需要用到唯一一个全局实例,这种情况下可以用以下方法代替std::call_once:
class my_class;
my_class& get_my_class_instance()
{
static my_class instance;
return instance;
}
3.2、保护不常更新的数据
如果我们想要允许单独一个“写线程”进行完全排他的访问,也允许多个“读线程”共享数据或并发访问,那么可以使用C++17提供的新互斥量std::shared_mutex。对于更新操作,使用std::lock_guard<std::shared_mutex>或std::unique_lock<std::shared_mutex>锁定,代替对应的std::mutex特化,它们都保证了访问的排他性质。对于无需更新数据结构的线程,可以另行改用共享锁std::shared_lock<std::shared_mutex>,多个线程能够同时锁住同一个std::shared_mutex。
class dns_entry {};
class dns_cache
{
std::map<std::string, dns_entry> entries;
std::shared_mutex entry_mutex;
public:
dns_entry find_entry(const std::string& domain)
{
std::shared_lock<std::shared_mutex> lk(entry_mutex);
auto it = entries.find(domain);
return it == entries.end() ? dns_entry() : it->second;
}
void update_or_add_entry(const std::string& domain, const dns_entry& dns_details)
{
std::lock_guard<std::shared_mutex> lk(entry_mutex);
entries[domain] = dns_details;
}
};
3.3、递归加锁
标准库提供了std::recursive_mutex,其工作方式与std::mutex相似,不同之处是其允许同一线程对某互斥量的同一实例多次加锁。假如我们对它调用3次lock(),就必须调用3次unlock()才能解锁。
《C++并发编程实战》读书笔记(2):线程间共享数据的更多相关文章
- Java并发编程实战 读书笔记(一)
最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一 线程与进程 进程与线程的解释 个人觉 ...
- Java并发编程实战 读书笔记(二)
关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...
- Disruptor 线程间共享数据无需竞争
队列的作用是缓冲 缓冲到 队列的空间里.. 线程间共享数据无需竞争 原文 地址 作者 Trisha 译者:李同杰 LMAX Disruptor 是一个开源的并发框架,并获得2011 Duke’ ...
- 详解 Qt 线程间共享数据(用信号槽方式)
使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享 ...
- Qt学习:线程间共享数据(使用信号槽传递数据,必须提前使用qRegisterMetaType来注册参数的类型)
Qt线程间共享数据主要有两种方式: 使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的: 使用singal/slot机制,把数据 ...
- 详解 Qt 线程间共享数据(使用signal/slot传递数据,线程间传递信号会立刻返回,但也可通过connect改变)
使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享 ...
- 《java并发编程实战》笔记
<java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为: Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...
- Java并发编程实践读书笔记(2)多线程基础组件
同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...
- JAVA并发编程实战---第二章:线程安全性
对象的状态是指存储在状态变量中的数据.对象的状态可能包括其他依赖对象的域.例如HashMap的状态不仅存储在HashMap本身,还存储在许多Map.Entry对象中.对象的状态中包含了任何可能影响其外 ...
- Java并发基础09. 多个线程间共享数据问题
先看一个多线程间共享数据的问题: 设计四个线程,其中两个线程每次对data增加1,另外两个线程每次对data减少1. 从问题来看,很明显涉及到了线程间通数据的共享,四个线程共享一个 data,共同操作 ...
随机推荐
- 工作中的技术总结_Thymeleaf插件_关于th:if 、th:with、th:replace和th:fragment的一些事 _20210825
工作中的技术总结_Thymeleaf _20210825 1.值取为空的情况:不能使用 th:if 进行条件渲染(因为是伪条件渲染,不管怎样元素都是先渲染到DOM再决定是否显示:个人这么认为不一定准确 ...
- 使用pandas进行数据分析
目录 1.pandas的特点 2.Series 2.1新建Seriws 2.2使用标签来选择数据 2.3 通过指定位置选择数据 2.4 使用布尔值选择数据 2.5 其他操作 2.5.1 修改数据 2. ...
- 历史性突破:独立开发 .net core 在线客服系统累计处理聊天消息 48 万余条!
业余时间用 .net core 写了一个在线客服系统.我把这款业余时间写的小系统丢在网上,陆续有人找我要私有化版本,我都给了,毕竟软件业的初衷就是免费和分享. 后来我索性就发了一个100%私有化版直接 ...
- MoeCTF2024--Crypto--Week1&Week2
MOECTF (CRYPTO) Week1: 1.入门指北 题目: from Crypto.Util.number import bytes_to_long, getPrime from secret ...
- PCI 设备 RTX 驱动开发方法 -5565反射内存
PCI 设备 RTX 驱动开发方法 RTX 下操作外设时, 需要把设备从 Windows 移动到 RTX 下面,具体移植方法可以参考 RTX Help 文档中间 Converting a Window ...
- PC大屏自适应 - 简洁版
PC大屏自适应通常做法 一般pc端页面布局会取中间一定的宽度,高度自适应.而可视化大屏需要在不同分辨率的显示屏上铺满整个屏幕,这就需要根据屏幕不同分辨率设置不同的宽高也就是自适应布局.在此向小伙伴们推 ...
- Vim基本使用指南
一般模式:移动光标的方法 h或 向左方向键(←)光标向左移动一个字符 j或 向下方向键(↓)光标向下移动一个字符 k或 向上方向键(↑)光标向上移动一个字符 l或 向右方向键(→)光标向右移动一个字符 ...
- Mac之终端工具iterm2
1. 安装 iTerm2 下载地址:https://www.iterm2.com/downloads.html 下载的是压缩文件,解压后是执行程序文件,你可以直接双击,或者直接将它拖到 Applica ...
- ssh之秘钥登陆
前提: 1. 秘钥的生成需要OpenSSL的支持, 需要自行进行安装 一. 新建用户 在root登陆状态中执行命令: useradd -m ssh-user # centosadduser ssh-u ...
- Javascript 标签的属性
1.为HTML标签设置和添加属性 setAttribute() setAttribute()方法可以给HTML标签设置/添加属性(原生的属性或者自定义的属性都可以)添加的属性会存储在标签中 <! ...