C++17 STL Cook Book
  • Introduction
  • 前言
  • 关于本书
  • 各章梗概
  • 第1章 C++17的新特性
    • 使用结构化绑定来解包绑定的返回值
    • 将变量作用域限制在if和switch区域内
    • 新的括号初始化规则
    • 构造函数自动推导模板的类型
    • 使用constexpr-if简化编译
    • 只有头文件的库中启用内联变量
    • 使用折叠表达式实现辅助函数
  • 第2章 STL容器
    • 擦除/移除std::vector元素
    • 以O(1)的时间复杂度删除未排序std::vector中的元素
    • 快速或安全的访问std::vector实例的方法
    • 保持对std::vector实例的排序
    • 向std::map实例中高效并有条件的插入元素
    • 了解std::map::insert新的插入提示语义
    • 高效的修改std::map元素的键值
    • std::unordered_map中使用自定义类型
    • 过滤用户的重复输入,并以字母序将重复信息打印出——std::set
    • 实现简单的逆波兰表示法计算器——std::stack
    • 实现词频计数器——std::map
    • 实现写作风格助手用来查找文本中很长的句子——std::multimap
    • 实现个人待办事项列表——std::priority_queue
  • 第3章 迭代器
    • 建立可迭代区域
    • 让自己的迭代器与STL的迭代器兼容
    • 使用迭代适配器填充通用数据结构
    • 使用迭代器实现算法
    • 使用反向迭代适配器进行迭代
    • 使用哨兵终止迭代
    • 使用检查过的迭代器自动化检查迭代器代码
    • 构建zip迭代适配器
  • 第4章 Lambda表达式
    • 使用Lambda表达式定义函数
    • 使用Lambda为std::function添加多态性
    • 并置函数
    • 通过逻辑连接创建复杂谓词
    • 使用同一输入调用多个函数
    • 使用std::accumulate和Lambda函数实现transform_if
    • 编译时生成笛卡尔乘积
  • 第5章 STL基础算法
    • 容器间相互复制元素
    • 容器元素排序
    • 从容器中删除指定元素
    • 改变容器内容
    • 在有序和无序的vector中查找元素
    • 将vector中的值控制在特定数值范围内——std::clamp
    • 在字符串中定位模式并选择最佳实现——std::search
    • 对大vector进行采样
    • 生成输入序列的序列
    • 实现字典合并工具
  • 第6章 STL算法的高级使用方式
    • 使用STL算法实现单词查找树类
    • 使用树实现搜索输入建议生成器
    • 使用STL数值算法实现傅里叶变换
    • 计算两个vector的误差和
    • 使用ASCII字符曼德尔布罗特集合
    • 实现分割算法
    • 将标准算法进行组合
    • 删除词组间连续的空格
    • 压缩和解压缩字符串
  • 第7章 字符串, 流和正则表达
    • 创建、连接和转换字符串
    • 消除字符串开始和结束处的空格
    • 无需构造获取std::string
    • 从用户的输入读取数值
    • 计算文件中的单词数量
    • 格式化输出
    • 使用输入文件初始化复杂对象
    • 迭代器填充容器——std::istream
    • 迭代器进行打印——std::ostream
    • 使用特定代码段将输出重定向到文件
    • 通过集成std::char_traits创建自定义字符串类
    • 使用正则表达式库标记输入
    • 简单打印不同格式的数字
    • 从std::iostream错误中获取可读异常
  • 第8章 工具类
    • 转换不同的时间单位——std::ratio
    • 转换绝对时间和相对时间——std::chrono
    • 安全的标识失败——std::optional
    • 对元组使用函数
    • 使用元组快速构成数据结构
    • 将void*替换为更为安全的std::any
    • 存储不同的类型——std::variant
    • 自动化管理资源——std::unique_ptr
    • 处理共享堆内存——std::shared_ptr
    • 对共享对象使用弱指针
    • 使用智能指针简化处理遗留API
    • 共享同一对象的不同成员
    • 选择合适的引擎生成随机数
    • 让STL以指定分布方式产生随机数
  • 第9章 并行和并发
    • 标准算法的自动并行
    • 让程序在特定时间休眠
    • 启动和停止线程
    • 打造异常安全的共享锁——std::unique_lock和std::shared_lock
    • 避免死锁——std::scoped_lock
    • 同步并行中使用std::cout
    • 进行延迟初始化——std::call_once
    • 将执行的程序推到后台——std::async
    • 实现生产者/消费者模型——std::condition_variable
    • 实现多生产者/多消费者模型——std::condition_variable
    • 并行ASCII曼德尔布罗特渲染器——std::async
    • 实现一个小型自动化并行库——std::future
  • 第10章 文件系统
    • 实现标准化路径
    • 使用相对路径获取规范的文件路径
    • 列出目录下的所有文件
    • 实现一个类似grep的文本搜索工具
    • 实现一个自动文件重命名器
    • 实现一个磁盘使用统计器
    • 计算文件类型的统计信息
    • 实现一个工具:通过符号链接减少重复文件,从而控制文件夹大小
Powered by GitBook
On this page
  • How to do it...
  • How it works...

Was this helpful?

  1. 第9章 并行和并发

避免死锁——std::scoped_lock

Previous打造异常安全的共享锁——std::unique_lock和std::shared_lockNext同步并行中使用std::cout

Last updated 6 years ago

Was this helpful?

如果在路上发生了死锁,就会像下图一样:

为了让交通顺畅,可能需要一个大型起重机,将路中间的一辆车挪到其他地方去。如果找不到起重机,那么我们就希望这些司机们能互相配合。当几个司机愿意将车往后退,留给空间给其他车通行,那么每辆车就不会停在原地了。

多线程编程中,开发者肯定需要避免这种情况的发生。不过,程序比较复杂的情况下,这种情况其实很容易发生。

本节中,我们将会故意的创造一个死锁的情况。然后,在相同资源的情况下,如何创造出一个死锁的情形。再使用C++17中,STL的std::scoped_lock如何避免死锁的发生。

How to do it...

本节中有两对函数要在并发的线程中执行,并且有两个互斥量。其中一对制造死锁,另一对解决死锁。主函数中,我们将使用这两个互斥量:

  1. 包含必要的头文件,并声明所使用的命名空间:

    #include <iostream>
    #include <thread>
    #include <mutex>
    
    using namespace std;
    using namespace chrono_literals;
  2. 实例化两个互斥量对象,制造死锁:

    mutex mut_a;
    mutex mut_b;
  3. 为了使用两个互斥量制造死锁,我们需要有两个函数。其中一个函数试图对互斥量A进行上锁,然后对互斥量B进行上锁,而另一个函数则试图使用相反的方式运行。让两个函数在等待锁时进行休眠,我们确定这段代码永远处于一个死锁的状态。(这就达到了我们演示的目的。当我们重复运行程序,那么程序在没有任何休眠代码的同时,可能会有成功运行的情况。)需要注意的是,这里我们没有使用\n字符作为换行符,我们使用的是endl。endl会输出一个换行符,同时也会对cout的流缓冲区进行刷新,所以我们可以确保打印信息不会有延迟或同时出现:

    static void deadlock_func_1()
    {
        cout << "bad f1 acquiring mutex A..." << endl;
    
        lock_guard<mutex> la {mut_a};
    
        this_thread::sleep_for(100ms);
    
        cout << "bad f1 acquiring mutex B..." << endl;
    
        lock_guard<mutex> lb {mut_b};
    
        cout << "bad f1 got both mutexes." << endl;
    }
  4. deadlock_func_2和deadlock_func_1看起来一样,就是A和B的顺序相反:

    static void deadlock_func_2()
    {
        cout << "bad f2 acquiring mutex B..." << endl;
    
        lock_guard<mutex> lb {mut_b};
    
        this_thread::sleep_for(100ms);
    
        cout << "bad f2 acquiring mutex A..." << endl;
    
        lock_guard<mutex> la {mut_a};
    
        cout << "bad f2 got both mutexes." << endl;
    }
  5. 现在我们将完成与上面函数相比,两个无死锁版本的函数。它们使用了scoped_lock,其会作为构造函数参数的所有互斥量进行上锁。其析构函数会进行解锁操作。锁定这些互斥量时,其内部应用了避免死锁的策略。这里需要注意的是,两个函数还是对A和B互斥量进行操作,并且顺序相反:

    static void sane_func_1()
    {
        scoped_lock l {mut_a, mut_b};
    
        cout << "sane f1 got both mutexes." << endl;
    }
    
    static void sane_func_2()
    {
        scoped_lock l {mut_b, mut_a};
    
        cout << "sane f2 got both mutexes." << endl;
    }
  6. 主函数中观察这两种情况。首先,我们使用不会死锁的函数:

    int main()
    {
        {
            thread t1 {sane_func_1};
            thread t2 {sane_func_2};
    
            t1.join();
            t2.join();
        }
  7. 然后,调用制造死锁的函数:

        {
            thread t1 {deadlock_func_1};
            thread t2 {deadlock_func_2};
    
            t1.join();
            t2.join();
        }
    }
  8. 编译并运行程序,就能得到如下的输出。前两行为无死锁情况下,两个函数的打印结果。接下来的两个函数则产生死锁。因为我们能看到f1函数始终是在等待互斥量B,而f2则在等待互斥量A。两个函数都没做成功的对两个互斥量上锁。我们可以让这个程序持续运行,不管时间是多久,结果都不会变化。程序只能从外部进行杀死,这里我们使用Ctrl + C的组合键,将程序终止:

    $ ./avoid_deadlock
    sane f1 got both mutexes
    sane f2 got both mutexes
    bad f2 acquiring mutex B...
    bad f1 acquiring mutex A...
    bad f1 acquiring mutex B...
    bad f2 acquiring mutex A...

How it works...

例子中,我们故意制造了死锁,我们也了解了这样一种情况发生的有多快。在一个很大的项目中,多线程开发者在编写代码的时候,都会共享一些互斥量用于保护资源,所有开发者都需要遵循同一种加锁和解锁的顺序。这种策略或规则是很容易遵守的,不过也是很容易遗忘的。另一个问题则是锁序倒置。

scoped_lock对于这种情况很有帮助。其实在C++17中添加,其工作原理与lock_guard和unique_lock一样:其构造函数会进行上锁操作,并且析构函数会对互斥量进行解锁操作。scoped_lock特别之处是,可以指定多个互斥量。

scoped_lock使用std::lock函数,其会调用一个特殊的算法对所提供的互斥量调用try_lock函数,这是为了避免死锁。因此,在加锁与解锁的顺序相同的情况下,使用scoped_lock或对同一组锁调用std::lock都是非常安全的。