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::unique_lock和std::shared_lock

由于对于线程的操作严重依赖于操作系统,所以STL提供与系统无关的接口是非常明智的,当然STL也会提供线程间的同步操作。这样就不仅是能够启动和停止线程,使用STL库也能完成线程的同步操作。

本节中,我们将了解到STL中的互斥量和RAII锁。我们使用这些工具对线程进行同步时,也会了解STL中更多同步辅助的方式。

How to do it...

我们将使用std::shared_mutex在独占(exclusive)和共享(shared)模式下来完成一段程序,并且也会了解到这两种方式意味着什么。另外,我们将不会对手动的对程序进行上锁和解锁的操作,这些操作都交给RAII辅助函数来完成:

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

    #include <iostream>
    #include <shared_mutex>
    #include <thread>
    #include <vector>
    
    using namespace std;
    using namespace chrono_literals;
  2. 整个程序都会围绕共享互斥量展开,为了简单,我们定义了一个全局实例:

    shared_mutex shared_mut;
  3. 接下来,我们将会使用std::shared_lock和std::unique_lock这两个RAII辅助者。为了让其类型看起来没那么复杂,这里进行别名操作:

    using shrd_lck = shared_lock<shared_mutex>;
    using uniq_lck = unique_lock<shared_mutex>;
  4. 开始写主函数之前,先使用互斥锁的独占模式来实现两个辅助函数。下面的函数中,我们将使用unique_lock实例来作为共享互斥锁。其构造函数的第二个参数defer_lock会告诉对象让锁处于解锁的状态。否则,构造函数会尝试对互斥量上锁阻塞程序,直至成功为止。然后,会对exclusive_lock的成员函数try_lock进行调用。该函数会立即返回,并且返回相应的布尔值,布尔值表示互斥量是否已经上锁,或是在其他地方已经锁住:

    static void print_exclusive()
    {
        uniq_lck l {shared_mut, defer_lock};
    
        if (l.try_lock()) {
            cout << "Got exclusive lock.\n";
        } else {
            cout << "Unable to lock exclusively.\n";
        }
    }
  5. 另一个函数也差不多。其会将程序阻塞,直至其获取相应的锁。然后,会使用抛出异常的方式来模拟发生错误的情况(只会返回一个整数,而非一个非常复杂的异常对象)。虽然,其会立即退出,并且在上下文中我们获取了一个锁住的互斥量,但是这个互斥量也可以被释放。这是因为unique_lock的析构函数在任何情况下都会将对应的锁进行释放:

    static void exclusive_throw()
    {
        uniq_lck l {shared_mut};
        throw 123;
    }
  6. 现在,让我们来写主函数。首先,先开一个新的代码段,并且实例化一个shared_lock实例。其构造函数将会立即对共享模式下的互斥量进行上锁。我们将会在下一步了解到这一动作的意义:

    int main()
    {
        {
            shrd_lck sl1 {shared_mut};
    
            cout << "shared lock once.\n";
  7. 现在我们开启另一个代码段,并使用同一个互斥量实例化第二个shared_lock实例。现在具有两个shared_lock实例,并且都具有同一个互斥量的共享锁。实际上,可以使用同一个互斥量实例化很多的shared_lock实例。然后,调用print_exclusive,其会尝试使用互斥量的独占模式对互斥量进行上锁。这样的调用当然不会成功,因为互斥量已经在共享模式下锁住了:

            {
                shrd_lck sl2 {shared_mut};
    
                cout << "shared lock twice.\n";
    
                print_exclusive();
            }
  8. 离开这个代码段后,shared_locks12的析构函数将会释放互斥量的共享锁。print_exclusive函数还是失败,这是因为互斥量依旧处于共享锁模式:

            cout << "shared lock once again.\n";
    
            print_exclusive();
        }
        cout << "lock is free.\n";
  9. 离开这个代码段时,所有shared_lock对象就都被销毁了,并且互斥量再次处于解锁状态,现在我们可以在独占模式下对互斥量进行上锁了。调用exclusive_throw,然后调用print_exclusive。不过因为unique_lock是一个RAII对象,所以是异常安全的,也就是无论exclusive_throw返回了什么,互斥量最后都会再次解锁。这样即便是互斥量处于锁定状态,print_exclusive 也不会被错误的状态所阻塞:

        try {
               exclusive_throw();
        } catch (int e) {
            cout << "Got exception " << e << '\n';
        }
    
        print_exclusive();
    }
  10. 编译并运行这段代码则会得到如下的输出。前两行展示的是两个共享锁实例。然后,print_exclusive函数无法使用独占模式对互斥量上锁。在离开内部代码段后,第二个共享锁解锁,print_exclusive函数依旧会失败。在离开这个代码段后,将会对互斥量所持有的锁进行释放,这样exclusive_throw和print_exclusive最终才能对互斥量进行上锁:

    $ ./shared_lock
    shared lock once.
    shared lock twice.
    Unable to lock exclusively.
    shared lock once again.
    Unable to lock exclusively.
    lock is free.
    Got exception 123
    Got exclusive lock.

How it works...

查阅C++文档时,我们会对不同的互斥量和RAII辅助锁有些困惑。在我们回看这节的代码之前,让我们来对STL的这两个部分进行总结。

互斥量

其为mutual exclusion的缩写。并发时不同的线程对于相关的共享数据同时进行修改时,可能会造成结果错误,我们在这里就可以使用互斥量对象来避免这种情况的发生,STL提供了不同特性的互斥量。不过,这些互斥量的共同点就是具有lock和unlock两个成员函数。

一个互斥量在解锁状态下时,当有线程对其使用lock()时,这个线程就获取了互斥量,并对互斥量进行上锁。这样,但其他线程要对这互斥量进行上锁时,就会处于阻塞状态,知道第一个线程对该互斥量进行释放。std::mutex就可以做到。

这里将STL一些不同的互斥量进行对比:

类型名

描述

具有lock和unlock成员函数的标准互斥量。并提供非阻塞函数try_lock成员函数。

与互斥量相同,并提供try_lock_for和try_lock_until成员函数,其能在某个时间段内对程序进行阻塞。

与互斥量相同,不过当一个线程对实例进行上锁,其可以对同一个互斥量对象多次调用lock而不产生阻塞。持有线程可以多次调用unlock,不过需要和lock调用的次数匹配时,线程才不再拥有这个互斥量。

提供与timed_mutex和recursive_mutex的特性。

这个互斥量在这方面比较特殊,它可以被锁定为独占模式或共享模式。独占模式时,其与标准互斥量的行为一样。共享模式时,其他线程也可能在共享模式下对其进行上锁。其会在最后一个处于共享模式下的锁拥有者进行解锁时,整个互斥量才会解锁。其行为有些类似于shared_ptr,只不过互斥量不对内存进行管理,而是对锁的所有权进行管理。

同时具有shared_mutex和timed_mutex两种互斥量独占模式和共享模式的特性。

锁

线程对互斥量上锁之后,很多事情都变的非常简单,我们只需要上锁、访问、解锁三步就能完成我们想要做的工作。不过对于有些比较健忘的开发者来说,在上锁之后,很容易忘记对其进行解锁,或是互斥量在上锁状态下抛出一个异常,如果要对这个异常进行处理,那么代码就会变得很难看。最优的方式就是程序能够自动来处理这种事情。这种问题很类似与内存泄漏,开发者在分配内存之后,忘记使用delete操作进行内存释放。

内存管理部分,我们有unique_ptr,shared_ptr和weak_ptr。这些辅助类可以很完美帮我们避免内存泄漏。互斥量也有类似的帮手,最简单的一个就是std::lock_guard。使用方式如下:

void critical_function()
{
    lock_guard<mutex> l {some_mutex};

    // critical section
}

lock_guard的构造函数能接受一个互斥量,其会立即自动调用lock,构造函数会直到获取互斥锁为止。当实例进行销毁时,其会对互斥量再次进行解锁。这样互斥量就很难陷入到lock/unlock循环错误中。

C++17 STL提供了如下的RAII辅助锁。其都能接受一个模板参数,其与互斥量的类型相同(在C++17中,编译器可以自动推断出相应的类型):

名称

描述

这个类没有什么其他的,构造函数中调用lock,析构函数中调用unlock。

与lock_guard类似,构造函数支持多个互斥量。析构函数中会以相反的顺序进行解锁。

使用独占模式对互斥量进行上锁。构造函数也能接受一个参数用于表示超时到的时间,并不会让锁一直处于上锁的状态。其也可能不对互斥量进行上锁,或是假设互斥量已经锁定,或是尝试对互斥量进行上锁。另外,函数可以在unique_lock锁的声明周期中,对互斥量进行上锁或解锁。

与unique_lock类似,不过所有操作都是在互斥量的共享模式下进行操作。

lock_guard和scoped_lock只拥有构造和析构函数,unique_lock和shared_lock就比较复杂了,但也更为通用。我们将在本章的后续章节中了解到,这些类型如何用于更加复杂的情况。

现在我们来回看一下本节的代码。虽然,只在单线程的上下文中运行程序,但是我们可以了解到如何对辅助锁进行使用。shrd_lck类型为shared_lock<shared_mutex>的缩写,并且其允许我们在共享模式下对一个实例多次上锁。当sl1和sl2存在的情况下,print_exclusive无法使用独占模式对互斥量进行上锁。

现在来看看处于独占模式的上锁函数:

int main()
{
    {
        shrd_lck sl1 {shared_mut};
        {
            shrd_lck sl2 {shared_mut};
            print_exclusive();
        }
        print_exclusive();
    }

    try {
        exclusive_throw();
    } catch (int e) {
        cout << "Got exception " << e << '\n';
    }

    print_exclusive();
}

exclusive_throw的返回也比较重要,即便是抛出异常退出,exclusive_throw函数依旧会让互斥量再度锁上。

因为print_exclusive使用了一个奇怪的构造函数,我们就再来看一下这个函数:

void print_exclusive()
{
    uniq_lck l {shared_mut, defer_lock};

    if (l.try_lock()) {
        // ...
    }
}

这里我们不仅提供了shared_mut,还有defer_lock作为unique_lock构造函数的参数。defer_lock是一个空的全局对象,其不会对互斥量立即上锁,所以我们可以通过这个参数对unique_lock不同的构造函数进行选择。这样做之后,我们可以调用l.try_lock(),其会告诉我们有没有上锁。在互斥量上锁的情况下,就可以做些别的事情了。如果的确有机会获取锁,依旧需要析构函数对互斥量进行清理。

Previous启动和停止线程Next避免死锁——std::scoped_lock

Last updated 6 years ago

Was this helpful?

mutex
timed_mutex
recursive_mutex
recursive_timed_mutex
shared_mutex
shared_timed_mutex
lock_guard
scoped_lock
unique_lock
shared_lock