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::condition_variable

本节中,我们将使用多线程实现一个经典的生产者/消费者模型。其过程就是一个生产者线程将商品放到队列中,然后另一个消费者线程对这个商品进行消费。如果不需要生产,生产者线程休眠。如果队列中没有商品能够消费,消费者休眠。

这里两个线程都需要对同一个队列进行修改,所以我们需要一个互斥量来保护这个队列。

需要考虑的事情是:如果队列中没有商品了,那么消费者做什么呢?需要每秒对队列进行检查,直到看到新的商品吗?当然,我们可以通过生产者触发一些事件叫醒消费者,这样消费者就能在第一时间获取到新的商品。

C++11中提供了一个很不错的数据结构std::condition_variable,其很适合处理这样的情况。本节中,我们实现一个简单的生产者/消费者应用,来对这个数据结构进行使用。

How to do it...

我们将实现一个单生产者/消费者程序,每个角色都有自己的线程:

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

    #include <iostream>
    #include <queue>
    #include <tuple>
    #include <condition_variable>
    #include <thread>
    
    using namespace std;
    using namespace chrono_literals;
  2. 队列进行实例化,并且队列q里只放简单的数字。生产者将商品放入队列中,消费者将商品从队列中取出。为了进行同步,我们需要一个互斥量。这就需要我们对condition_variable进行实例化,其变量名为cv。finished变量将会告诉生产者,无需在继续生产:

    queue<size_t> q;
    mutex mut;
    condition_variable cv;
    bool finished {false};
  3. 首先,我们来实现一个生产者函数。其能接受一个参数items,其会限制生产者生产的最大数量。一个简单的循环中,我们将会隔100毫秒生产一个商品,这个耗时就是在模拟生产的复杂性。然后,我们会对队列的互斥量进行上锁,并同步的对队列进行访问。成功的生产后,将商品加入队列时,我们需要调用cv.notify_all(),函数会叫醒所有消费线程。我们将在后面看到消费者那边是如何工作的:

    static void producer(size_t items) {
        for (size_t i {0}; i < items; ++i) {
            this_thread::sleep_for(100ms);
            {
                lock_guard<mutex> lk {mut};
                q.push(i);
            }
            cv.notify_all();
        }
  4. 生产完所有商品后,我们会将互斥量再度上锁,因为需要对finished位进行设置。然后,再次调用cv.notify_all():

        {
            lock_guard<mutex> lk {mut};
            finished = true;
        }
        cv.notify_all();
    }
  5. 现在来实现消费者函数。因为只是对队列上的数值进行消费,直到消费完所有的数值,所以这个函数不需要参数。当finished未被设置时,循环会持续执行,并且会对保护队列的互斥量进行上锁,将对队列和finished标识同时进行保护。当互斥量上锁,则锁就会调用cv.wait,并以Lambda表达式为参数。这个Lambda表达式其实就是个条件谓词,如果生产者线程还在继续工作,并且还有商品在队列上,消费者线程就不能停下来:

    static void consumer() {
       while (!finished) {
           unique_lock<mutex> l {mut};
    
           cv.wait(l, [] { return !q.empty() || finished; });
  6. cv.wait的调用会对锁进行解锁,并且会等到给予的条件达成时才会继续运行。然后,其会再次对互斥量上锁,并对队列上的商品进行消费,直到队列为空。如果生成者还在继续生成,那么这个循环可能会一直进行下去。否则,当finished被设置时,循环将会终止,这也就表示生产者不会再进行生产:

            while (!q.empty()) {
                cout << "Got " << q.front()
                    << " from queue.\n";
                q.pop();
            }
        }
    }
  7. 主函数中,我们让生产者生产10个商品。然后,我们就等待程序的结束:

    int main() {
        thread t1 {producer, 10};
        thread t2 {consumer};
        t1.join();
        t2.join();
        cout << "finished!\n";
    }
  8. 编译并运行程序,我们将会得到下面的输出。当程序在运行阶段时,我们将看到每一行,差不多隔100毫秒打印出来,因为生产者需要时间进行生产:

    $ ./producer_consumer
    Got 0 from queue.
    Got 1 from queue.
    Got 2 from queue.
    Got 3 from queue.
    Got 4 from queue.
    Got 5 from queue.
    Got 6 from queue.
    Got 7 from queue.
    Got 8 from queue.
    Got 9 from queue.
    finished!

How it works...

本节中,我们只启动了两个线程。第一个线程会生产一些商品,并放到队列中。另一个则是从队列中取走商品。当其中一个线程需要对队列进行访问时,其否需要对公共互斥量mut进行上锁,这样才能对队列进行访问。这样,我们就能保证两个线程不能再同时对队列进行操作。

除了队列和互斥量,我们还声明了2个变量,其也会对生产者和消费者有所影响:

queue<size_t> q;
mutex mut;
condition_variable cv;
bool finished {false};

finished变量很容易解释。当其设置为true时,生产者则会对固定数量的产品进行生产。当消费者看到这个值为true的时候,其就要将队列中的商品全部消费完。但是condition_variable cv代表了什么呢?我们在两个不同上下文中使用cv。其中一个上下文则会去等待一个特定的条件,并且另一个会达成对应的条件。

消费者这边将会等待一个特殊的条件。消费者线程会在对互斥量mut使用unique_lock上锁后,进行阻塞循环。然后,会调用cv.wait:

while (!finished) {
    unique_lock<mutex> l {mut};

    cv.wait(l, [] { return !q.empty() || finished; });

    while (!q.empty()) {
        // consume
    }
}

我们写了下面一段代码,这上下来两段代码看起来是等价的。我们会在后面了解到,这两段代码真正的区别到底在哪里:

while (!finished) {
    unique_lock<mutex> l {mut};

    while (q.empty() && !finished) {
        l.unlock();
        l.lock();
    }

    while (!q.empty()) {
        // consume
    }
}

这就意味着,我们先要进行上锁,然后对我们的应对方案进行检查:

  1. 还有商品能够消费吗?有的话,继续持有锁,消费,释放锁,结束。

  2. 如果没有商品可以消费,但是生产者依旧存在,释放互斥锁,也就是给生产者一个机会向队列中添加商品。然后,尝试再对现状进行检查,如果现状有变,则跳转到1方案中。

cv.wait为什么与while(q.empty() && ... )不等价呢?因为在wait不需要再循环中持续的进行上锁和解锁的循环。如果生产者线程处于未激活状态时,这就会导致互斥量持续的被上锁和解锁,这样的操作是没有意义的,而且还会耗费掉不必要的CPU周期。

cv.wait(lock, predicate)将会等到predicate()返回true时,结束等待。不过其不会对lock持续的进行解锁与上锁的操作。为了将使用wait阻塞的线程唤醒,我们就需要使用condition_variable 对象,另一个线程会对同一个对象调用notify_one()或notify_all()。等待中的线程将从休眠中醒来,并检查predicate()条件是否成立。

wait还有一个很好的地方在于,如果出现了伪唤醒操作,那么线程则会再次进行休眠状态。这也就是当我们发出了过多的叫醒信号时,其不会对程序流有任何影响(但是会影响到性能)。

生产者端,在向队列输出商品后,调用cv.notify_all(),并且在生产最后一个商品时,将finished设置为true,这就等于引导了消费者进行消费。

Previous将执行的程序推到后台——std::asyncNext实现多生产者/多消费者模型——std::condition_variable

Last updated 6 years ago

Was this helpful?