更多的阅读笔记,及示例代码见 Github

https://github.com/anlongstory/C-_Concurrency_in_Action_reading_notes

本章主要内容:

   想象一下,你和你的朋友合租一个公寓,公寓中只有一个厨房和一个卫生间。当你的朋友在卫生间时,你就会不能使用了(除非你们特别好,好到可以在同时使用一个房间)。这个问题也会出现在厨房,假如:厨房里有一个组合式烤箱,当在烤香肠的时候,也在做蛋糕,就可能得到我们不想要的食物(香肠味的蛋糕)。此外,在公共空间将一件事做到一半时,发现某些需要的东西被别人借走,或是当离开的一段时间内有些东西被变动了地方,这都会令我们不爽。同样的问题,也困扰着线程。当线程在访问共享数据的时候,必须定一些规矩,用来限定线程可访问的数据位。还有,一个线程更新了共享数据,需要对其他线程进行通知。从易用性的角度,同一进程中的多个线程进行数据共享,有利有弊。错误的共享数据使用是产生并发bug的一个主要原因,并且后果要比香肠味的蛋糕更加严重。

3.1 共享数据带来的问题

  如上图,一个双链表每一个节点有两个指针分别指向前一个和后一个节点,这两个节点称为不变量。当涉及到某一个线程需要修改共享数据时,例如删除操作,当只有其中一边更新,不变量就被破坏了,直到另一边也完成更新,不变量就又稳定了,当一个线程只完成其中一边更新,另一个线程刚好需要访问这个被删除的节点,或者另一个线程也在尝试要删除这个节点,就会有问题出现,这是常见的错误,条件竞争。

3.1.1 条件竞争

  上面例子可以看出,当不变量遭到破坏时,才会产生条件竞争,当系统负载增加时,随着执行数量的增加,执行序列的问题复现的概率也在增加,条件竞争通常是时间敏感的,所以程序以调试模式运行时,它们常会完全消失,因为调试模式会影响程序的执行时间(即使影响不多)。

3.1.2 避免恶性条件竞争
  • 最简单的方法就是对数据结构采用某种保护机制,确保只有进行修改的线程才能看到不变量被破坏时的中间状态。从其他访问线程的角度来看,修改不是已经完成了,就是还没开始。
  • 对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化,也就是保证每个不变量保持稳定的状态,即无锁编程。
  • 另一种处理条件竞争的方式是,使用事务的方式去处理数据结构的更新(这里的"处理"就如同对数据库进行更新一样)

保护共享数据结构的最基本的方式,是使用C++标准库提供的互斥量。

3.2 使用互斥量保护共享数据

  当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据,而不破坏不变量。

3.2.1 C++中使用互斥量

  C++中通过实例化 std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去调用unlock(),也包括异常的情况。
  C++中提供了模板类 std::lock_guard,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。std::mutex 和 std::lock_guard 都在 <mutex> 头文件中声明。

#include<iostream>
#include<list>
#include<mutex>
#include <algorithm>std::list<int> some_list;  // 全局变量
std::mutex some_mutex;  // 全局互斥锁void add_to_list(int new_value)
{std::lock_guard<std::mutex> guard(some_mutex);  // 1some_list.push_back(new_value);
}bool list_contains(int value_to_find)
{std::lock_guard<std::mutex> guard(some_mutex);  // 2returnstd::find(some_list.begin(), some_list.end(), value_to_find) != some_list.end();
}

在上面 1 和 2 两个函数处使用了 std::lock_guardstd::mutex,使得这两个函数对数据的访问是互斥的:2的函数不能看到正在被 1 函数修改的列表。一般的,可以将其封装成类,互斥量和要保护的数据,在类中都需要定义为private成员,这会让访问数据的代码变的清晰,并且容易看出在什么时候对互斥量上锁。

3.2.2 精心组织代码来保护共享数据

切勿将受保护数据的指针或引用传递到互斥锁作用域之外,无论是函数返回值,还是存储在外部可见内存,亦或是以参数的形式传递到用户提供的函数中去。

3.2.3 发现接口内在的条件竞争
3.2.4 死锁:问题描述及解决方案

死锁是指不同的两个线程会互相等待,从而什么都没做的情况。避免死锁的一般建议,就是让两个互斥量总以相同的顺序上锁:总在互斥量B之前锁住互斥量A,就永远不会死锁。不过,选择一个固定的顺序(例如,实例提供的第一互斥量作为第一个参数,提供的第二个互斥量为第二个参数),可能会适得其反:在参数交换了之后,两个线程试图在相同的两个实例间进行数据交换时,程序又死锁了!很幸运,C++标准库有办法解决这个问题, std::lock ——可以一次性锁住多个(两个以上)的互斥量,并且没有副作用(死锁风险)。

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);  // 调用 lock()锁住两个互斥量// 提供 std::adopt_lock 参数除了表示 std::lock_guard 对象可获取锁之外,还将锁交由 // std::lock_guard 对象管理,而不需要 std::lock_guard 对象再去构建新的锁。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, ths.some_detail);}
};

std::lock 要么将两个锁都锁住,要不一个都不锁。虽然 std::lock 可以在这情况下(获取两个以上的锁)避免死锁,但它没办法帮助你获取其中一个锁。

3.2.5 避免死锁的进阶指导
  • 避免嵌套锁
  • 避免在持有锁时调用用户提供的代码
  • 使用固定顺序获取锁
  • 使用锁的层次结构
3.2.6 std::unique_lock——灵活的锁

std::unqiue_lock 使用更为自由的不变量,这样 std::unique_lock 实例不会总与互斥量的数据类型相关,使用起来要比 std:lock_guard 更加灵活。首先,可将 std::adopt_lock 作为第二个参数传入构造函数,对互斥量进行管理;也可以将 std::defer_lock 作为第二个参数传递进去,表明互斥量应保持解锁状态。这样,就可以被 std::unique_lock 对象(不是互斥量)的lock()函数的所获取,或传递 std::unique_lock 对象到 std::lock() 中。

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::def_lock 留下未上锁的互斥量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, ths.some_detail);}
};

std::unique_lock 对象的体积通常要比 std::lock_guard 对象大,当使用 std::unique_lock 替代 std::lock_guard ,因为会对标志进行适当的更新或检查,就会做些轻微的性能惩罚。当 std::lock_guard 已经能够满足你的需求,那么还是建议你继续使用它。当需要更加灵活的锁时,最好选择 std::unique_lock

3.2.7 不同域中互斥量所有权的传递
3.2.8 锁的粒度

锁的粒度是用来描述一个锁保护着的数据量大小。细粒度锁表示能够保护较小的数据量,粗粒度锁表示能够保护较多的数据量。如果很多线程正在等待同一个资源(等待收银员对自己拿到的商品进行清点),当有线程持有锁的时间过长,这就会增加等待的时间(别等到结账的时候,才想起来蔓越莓酱没拿)。在可能的情况下,锁住互斥量的同时只能对共享数据进行访问;试图对锁外数据进行处理。特别是做一些费时的动作,比如:对文件的输入/输出操作进行上锁。文件输入/输出通常要比从内存中读或写同样长度的数据慢成百上千倍,所以除非锁已经打算去保护对文件的访问,要么执行输入/输出操作将会将延迟其他线程执行的时间,这很没有必要(因为文件锁阻塞住了很多操作),这样多线程带来的性能效益会被抵消。锁不仅是能锁住合适粒度的数据,还要控制锁的持有时间,以及什么操作在执行的同时能够拥有锁。

3.3 保护共享数据的替代设施

互斥量是最通用的机制,但其并非保护共享数据的唯一方式。这里有很多替代方式可以在特定情况下,提供更加合适的保护。

3.3.1 保护共享数据的初始化过程

C++标准库提供了 std::once_flagstd::call_once 来处理这种情况。比起锁住互斥量,并显式的检查指针,每个线程只需要使用 std::call_once ,在 std::call_once 的结束时,就能安全的知道指针已经被其他的线程初始化了。使用 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::once_flag 和初始化好的数据都是命名空间区域的对象,但是 std::call_once()可仅作为延迟初始化的类型成员。

3.3.2 保护很少更新的数据结构

比起使用 std::mutex 实例进行同步,不如使用 boost::shared_mutex 来做同步。对于更新操作,可以使用 std::lock_guard<boost::shared_mutex>std::unique_lock<boost::shared_mutex> 上锁。作为 std::mutex 的替代方案,与 std::mutex 所做的一样,这就能保证更新线程的独占访问。因为其他线程不需要去修改数据结构,所以其可以使用 boost::shared_lock<boost::shared_mutex> 获取访问权。这与使用 std::unique_lock 一样,除非多线程要在同时获取同一个boost::shared_mutex 上有共享锁。唯一的限制:当任一线程拥有一个共享锁时,这个线程就会尝试获取一个独占锁,直到其他线程放弃他们的锁;同样的,当任一线程拥有一个独占锁时,其他线程就无法获得共享锁或独占锁,直到第一个线程放弃其拥有的锁。

3.3.3 嵌套锁

当一个线程已经获取一个 std::mutex 时(已经上锁),并对其再次上锁,这个操作就是错误的,并且继续尝试这样做的话,就会产生未定义行为。然而,在某些情况下,一个线程尝试获取同一个互斥量多次,而没有对其进行一次释放是可以的。之所以可以,是因为 C++ 标准库提供了 std::recursive_mutex 类。其功能与 std::mutex 类似,除了你可以从同一线程的单个实例上获取多个锁。互斥量锁住其他线程前,你必须释放你拥有的所有锁,所以当你调用lock()三次时,你也必须调用unlock()三次。正确使用 std::lock_guard<std::recursive_mutex>std::unique_lock<std::recursive_mutex> 可以帮你处理这些问题。

《C++并发编程实战》读书笔记——chapter 3_线程间共享数据相关推荐

  1. Java并发编程实战读书笔记(一)——线程安全性、对象共享

    一.线程安全性 一个对象是否需要是线程安全的,取决于它是否被多个线程访问. 当多个线程访问,并且其中有一个执行写入时,必须采用同步机制,Java中主要的同步关键字是 synchronized 独占加锁 ...

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

    Java并发编程 标签(空格分隔): 并发 多线程 基础 线程 在执行过程中,能够执行程序代码的一个执行单元,在Java语言中,线程有四种状态:运行,就绪,挂起,结束. 并发特性 原子性 一个操作不会 ...

  3. Java并发编程实战读书笔记三

    第七章 取消和关闭 Java没有提供任何机制来安全的终止线程,虽然 Thread.stop 和 suspend 等方法提供了这样的机制,但由于存在着一些严重的陷,因此应该避免使用 7.1任务取消 7. ...

  4. Java并发编程实战读书笔记二

    第五章 基础构建模块 5.1 同步容器类 5.1.1 同步容器类的问题 如下,如果list含有10个元素,线程A调用getLast的同时线程B调用deleteLast,那么getLast可能会报Arr ...

  5. 并发编程实战-读书笔记

    2019独角兽企业重金招聘Python工程师标准>>> 原子性 ++count  "读取-修改-写入" 竞态条件 先检查后执行Check-Then-Act,通过一 ...

  6. Java并发编程实战读书笔记一

    第1章 简介 第2章 线程安全性 1个状态变量线程安全的模式 多个状态变量线程不安全的模式,在A线程lastNumbers.set和lastFactors.set之间B线程进行这两个set就出问题了, ...

  7. C++并发编程线程间共享数据std::future和sd::promise

    线程间共享数据 使用互斥锁实现线程间共享数据 为了避免死锁可以考虑std::lock()或者boost::shared_mutex 要尽量保护更少的数据 同步并发操作 C++标准库提供了一些工具 可以 ...

  8. java并发编程实践 读书笔记_Java - 并发编程实践(读书笔记)

    [注] 同步机制保证:1)原子性 2)内存可见性: Volatile变量只能保证:1)可见性: - 恰当的同步,同步的弱形式,确保对一个变量的更新以可预见的方式告知其他线程. [注] 用锁来协调访问变 ...

  9. Java 并发编程艺术 读书笔记

    第 1 章 并发编程的挑战 1.1.3 如何减少上下文切换 减少上下文切换的方法有无锁并发编程.CAS 算法.使用最少线程和使用协程. 无锁并发编程.多线程竞争锁时,会引起上下文切换,所以多线程处理数 ...

最新文章

  1. Linux下查看.so和可执行文件是否debug编译
  2. 域控服务器发生w32time错误
  3. ubuntu 进入 recovery mode
  4. Android 通过高德地图获取地址的经纬度
  5. mysql改变地址_mysql 修改数据库存储地址
  6. ICCV2021 workshop 多视角残缺点云的补全与配准
  7. Android判断网络状态
  8. 怎样恢复计算机程序打开文件名,Word提示如何解决使用文本恢复转换器打开文件的问题...
  9. C语言半框,不同的镜架结构优劣大盘点
  10. 中秋节到了,为什么你不回家?
  11. epub文件是什么文件?用这个方法直接在浏览器打开
  12. https 请求的端口是443 注意
  13. 如何实现一个“线程池”
  14. Red Hat 认证工程师(RHCE)
  15. PyTorch学习笔记(9)——nn.Conv2d和其中的padding策略
  16. C语言:一个球从100m高度自由落下,每次落地后反跳回原高度的一半,再落下,再反弹;求它在第10次落地时,共经过多少米,第10次反弹多高;
  17. ctf题库--1000
  18. PMP五大过程组与十大知识领域(九五之尊图)
  19. SAS中保留t值、F值和Z值的三位小数
  20. 我的 2019 总结:警钟为谁而鸣

热门文章

  1. 详述ArrayList的contains方法
  2. 访问者模式 java_java设计模式之访问者模式
  3. 平台资金提现解决方案之实现微信付款到银行卡功能
  4. Git 环境变量配置
  5. 2018四川卫生学校哪所好?
  6. 步行速度快慢测试软件,神奇!走路速度竟能测算你的寿命
  7. 信息论入门:信息守恒定律与纠错码
  8. v9 android8,尝鲜奥利奥:荣耀9/V9即将升级EMUI8.0+Android8.0
  9. Win11文件夹打不开怎么办
  10. 【JAVA】数据结构之最短路径问题