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):线程间共享数据的更多相关文章

  1. Java并发编程实战 读书笔记(一)

    最近在看多线程经典书籍Java并发变成实战,很多概念有疑惑,虽然工作中很少用到多线程,但觉得还是自己太弱了.加油.记一些随笔.下面简单介绍一下线程. 一  线程与进程   进程与线程的解释   个人觉 ...

  2. Java并发编程实战 读书笔记(二)

    关于发布和逸出 并发编程实践中,this引用逃逸("this"escape)是指对象还没有构造完成,它的this引用就被发布出去了.这是危及到线程安全的,因为其他线程有可能通过这个 ...

  3. Disruptor 线程间共享数据无需竞争

    队列的作用是缓冲 缓冲到 队列的空间里.. 线程间共享数据无需竞争 原文 地址  作者  Trisha   译者:李同杰 LMAX Disruptor 是一个开源的并发框架,并获得2011 Duke’ ...

  4. 详解 Qt 线程间共享数据(用信号槽方式)

    使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享 ...

  5. Qt学习:线程间共享数据(使用信号槽传递数据,必须提前使用qRegisterMetaType来注册参数的类型)

    Qt线程间共享数据主要有两种方式: 使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的: 使用singal/slot机制,把数据 ...

  6. 详解 Qt 线程间共享数据(使用signal/slot传递数据,线程间传递信号会立刻返回,但也可通过connect改变)

    使用共享内存.即使用一个两个线程都能够共享的变量(如全局变量),这样两个线程都能够访问和修改该变量,从而达到共享数据的目的. Qt 线程间共享数据是本文介绍的内容,多的不说,先来啃内容.Qt线程间共享 ...

  7. 《java并发编程实战》笔记

    <java并发编程实战>这本书配合并发编程网中的并发系列文章一起看,效果会好很多. 并发系列的文章链接为:  Java并发性和多线程介绍目录 建议: <java并发编程实战>第 ...

  8. Java并发编程实践读书笔记(2)多线程基础组件

    同步容器 同步容器是指那些对所有的操作都进行加锁(synchronize)的容器.比如Vector.HashTable和Collections.synchronizedXXX返回系列对象: 可以看到, ...

  9. JAVA并发编程实战---第二章:线程安全性

    对象的状态是指存储在状态变量中的数据.对象的状态可能包括其他依赖对象的域.例如HashMap的状态不仅存储在HashMap本身,还存储在许多Map.Entry对象中.对象的状态中包含了任何可能影响其外 ...

  10. Java并发基础09. 多个线程间共享数据问题

    先看一个多线程间共享数据的问题: 设计四个线程,其中两个线程每次对data增加1,另外两个线程每次对data减少1. 从问题来看,很明显涉及到了线程间通数据的共享,四个线程共享一个 data,共同操作 ...

随机推荐

  1. 工作中的技术总结_ thymeleaf的应用 _select&input的数据回显 _20210910

    工作中的技术总结_ thymeleaf的应用 _select&input的数据回显 _20210910 在需要用户输入的场合,常常会有对用户填入数据的验证,对数据的验证不通过则需要返回到表单页 ...

  2. .NET云原生应用实践(四):基于Keycloak的认证与授权

    本章目标 完成Keycloak的本地部署与配置 在Stickers RESTful API层面完成与Keycloak的集成 在Stickers RESTful API上实现认证与授权 Keycloak ...

  3. Docker 自定义镜像Dockerfile使用详细教程

    认识 Dockerfile 文件 Dockerfile 用于构建 Docker 镜像,Dockerfile 文件是由一行行命令语句 组成,基于这些命令即可以构建一个镜像 比如下面就是一个Dockefi ...

  4. 18.Kubernetes容器交付介绍

    Kubernetes容器交付介绍 如何在k8s集群中部署Java项目 容器交付流程 开发代码阶段 编写代码 编写Dockerfile[打镜像做准备] 持续交付/集成 代码编译打包 制作镜像 上传镜像仓 ...

  5. 低功耗4G模组Air780E快速入门:固件的远程升级

    ​ 今天我们学习Air780E快速入门之固件的远程升级,小伙伴们,学起来吧! 一.生成差分包 合宙的远程升级支持使用合宙云平台和自建服务器,此例程使用的是合宙云平台. 1.1 准备新旧版的core和脚 ...

  6. 能不能用uni开发一个线上运动会的APP、小程序?

    引言:uni-app凭借其强大的跨平台能力,成为开发AI运动类APP和小程序的首选框架.本文旨在探讨基于uni进行开发AI运动小程序.APP开发,以及开发过程中遇到的技术难点,并为您介绍一个开箱即用的 ...

  7. Solr 4.0 基础教程

    本文只是Solr 4.0的基础教程,本人不经常写东西,写的不好请见谅,欢迎到群233413850进行讨论学习. 转载请标明原文地址:http://my.oschina.net/zhanyu/blog/ ...

  8. Golang框架之gin

    gin是目前golang的主要web框架之一,之所以选择这个框架是因为其拥有高效的路由性能,并且有人长期维护,目前github上的star数已经破3W. [安装] go get -u github.c ...

  9. PHP之项目环境变量设置

    需求 在PHP开发中为了区分线上生产环境还是本地开发环境, 如果我们能通过判断$_SERVER['RUNTIME_ENVIROMENT']为 'DEV'还是'PRO'来区分该多好, 可惜的是$_SER ...

  10. Electron(2) - 下载与解压缩

    1.下载文件 主线程中调用下载 win.webContents.downloadURL(url) 监听下载事件 //监听下载动作 win.webContents.session.on('will-do ...