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...
  • There's more...

Was this helpful?

  1. 第9章 并行和并发

将执行的程序推到后台——std::async

当我们想要将一些可以执行的代码放在后台,可以用线程将这段程序运行起来。然后,我们就等待运行的结果就好:

std::thread t {my_function, arg1, arg2, ...};
// do something else
t.join(); // wait for thread to finish

这里t.join()并不会给我们my_function函数的返回值。为了获取返回值,需要先实现my_function函数,然后将其返回值存储到主线程能访问到的地方。如果这样的情况经常发生,我们就要重复的写很多代码。

C++11之后,std::async能帮我们完成这项任务。我们将写一个简单的程序,并使用异步函数,让线程在同一时间内做很多事情。std::async其实很强大,让我们先来了解其一方面。

How to do it...

我们将在一个程序中并发进行多个不同事情,不显式创建线程,这次使用std::async和std::future:

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

    #include <iostream>
    #include <iomanip>
    #include <map>
    #include <string>
    #include <algorithm>
    #include <iterator>
    #include <future>
    
    using namespace std;
  2. 实现了三个函数,算是完成些很有趣的任务。第一个函数能够接收一个字符串,并且创建一个对于字符串中的字符进行统计的直方图:

    static map<char, size_t> histogram(const string &s)
    {
        map<char, size_t> m;
    
        for (char c : s) { m[c] += 1; }
    
        return m;
    }
  3. 第二个函数也能接收一个字符串,并返回一个排序后的副本:

    static string sorted(string s)
    {
        sort(begin(s), end(s));
        return s;
    }
  4. 第三个函数会对传入的字符串中元音字母进行计数:

    static bool is_vowel(char c)
    {
        char vowels[] {"aeiou"};
        return end(vowels) !=
                find(begin(vowels), end(vowels), c);
    }
    
    static size_t vowels(const string &s)
    {
        return count_if(begin(s), end(s), is_vowel);
    }
  5. 主函数中,我们从标准输入中获取字符串。为了不让输入字符串分段,我们禁用了ios::skipws。这样就能得到一个很长的字符串,并且不管这个字符串中有多少个空格。我们会对结果字符串使用pop_back,因为这种方式会让一个字符串中包含太多的终止符:

    int main()
    {
        cin.unsetf(ios::skipws);
        string input {istream_iterator<char>{cin}, {}};
        input.pop_back();
  6. 为了获取函数的返回值,并加快对输入字符串的处理速度,我们使用了异步的方式。std::async函数能够接收一个策略和一个函数,以及函数对应的参数。我们对于这个三个函数均使用launch::async策略。并且,三个函数的输入参数是完全相同的:

        auto hist (async(launch::async,
                        histogram, input));
        auto sorted_str (async(launch::async,
                        sorted, input));
        auto vowel_count (async(launch::async,
                        vowels, input));
  7. async的调用会立即返回,因为其并没有执行我们的函数。另外,准备好同步的结构体,用来获取函数所返回的结果。目前的结果使用不同的线程并发的进行计算。此时,我们可以做其他事情,之后再来获取函数的返回值。hist,sorted_str和vowel_count分别为函数histogram,sorted 和vowels的返回值,不过其会通过std::async包装入future类型中。这个对象表示在未来某个时间点上,对象将会获取返回值。通过对future对象使用.get(),我们将会阻塞主函数,直到相应的值返回,然后再进行打印:

        for (const auto &[c, count] : hist.get()) {
            cout << c << ": " << count << '\n';
        }
    
        cout << "Sorted string: "
            << quoted(sorted_str.get()) << '\n'
            << "Total vowels: "
            << vowel_count.get() << '\n';
    }
  8. 编译并运行代码,就能得到如下的输出。我们使用一个简短的字符串的例子时,代码并不是真正的在并行,但这个例子中,我们能确保代码是并发的。另外,程序的结构与串行版本相比,并没有改变多少:

     $ echo "foo bar baz foobazinga" | ./async
      : 3
     a: 4
     b: 3
     f: 2
     g: 1
     i: 1
     n: 1
     o: 4
     r: 1
     z: 2
     Sorted string: "   aaaabbbffginoooorzz"
     Total vowels: 9

How it works...

如果你没有使用过std::async,那么代码可以简单的写成串行代码:

auto hist (histogram(input));
auto sorted_str (sorted( input));
auto vowel_count (vowels( input));

for (const auto &[c, count] : hist) {
  cout << c << ": " << count << '\n';
}
cout << "Sorted string: " << quoted(sorted_str) << '\n';
cout << "Total vowels: " << vowel_count << '\n';

下面的代码,则是并行的版本。我们将三个函数使用async(launch::async, ...)进行包装。这样三个函数都不会由主函数来完成。此外,async会启动新线程,并让线程并发的完成这几个函数。这样我们只需要启动一个线程的开销,就能将对应的工作放在后台进行,而后可以继续执行其他代码:

auto hist (async(launch::async, histogram, input));
auto sorted_str (async(launch::async, sorted, input));
auto vowel_count (async(launch::async, vowels, input));

for (const auto &[c, count] : hist.get()) {
    cout << c << ": " << count << '\n';
}

cout << "Sorted string: "
    << quoted(sorted_str.get()) << '\n'
    << "Total vowels: "
    << vowel_count.get() << '\n';

例如histogram函数则会返回一个map实例,async(..., histogram, ...)将返回给我们的map实例包装进之前就准备好的future对象中。future对象时一种空的占位符,直到线程执行完函数返回时,才有具体的值。结果map将会返回到future对象中,所以我们可以对对象进行访问。get函数能让我们得到被包装起来的结果。

让我们来看一个更加简单的例子。看一下下面的代码:

auto x (f(1, 2, 3));
cout << x;

与之前的代码相比,我们也可以以下面的方式完成代码:

auto x (async(launch::async, f, 1, 2, 3));
cout << x.get();

这都是最基本的。后台执行的方式可能要比标准C++出现还要早。当然,还有一个问题要解决:launch::async是什么东西?launch::async是一个用来定义执行策略的标识。其有两种独立方式和一种组合方式:

策略选择

意义

运行新线程,以异步执行任务

在调用线程上执行任务(惰性求值)。在对future调用get和wait的时候,才进行执行。如果什么都没有发生,那么执行函数就没有运行。

launch::async | launch::deferred

具有两种策略共同的特性,STL的async实现可以的选择策略。当没有提供策略时,这种策略就作为默认的选择。

Note:

不使用策略参数调用async(f, 1, 2, 3),我们将会选择都是用的策略。async的实现可以自由的选择策略。这也就意味着,我们不能确定任务会执行在一个新的线程上,还是执行在当前线程上。

There's more...

还有件事情我们必须要知道,假设我们写了如下的代码:

async(launch::async, f);
async(launch::async, g);

这就会让f和g函数并发执行(这个例子中,我们并不关心其返回值)。运行这段代码时,代码会阻塞在这两个调用上,这并不是我们想看到的情况。

所以,为什么会阻塞呢?async不是非阻塞式、异步的调用吗?没错,不过这里有点特殊:当对一个async使用launch::async策略时,获取一个future对象,之后其析构函数将会以阻塞式等待方式运行。

这也就意味着,这两次调用阻塞的原因就是,future生命周期只有一行的时间!我们可以以获取其返回值的方式,来避免这个问题,从而让future对象的生命周期更长。

Previous进行延迟初始化——std::call_onceNext实现生产者/消费者模型——std::condition_variable

Last updated 6 years ago

Was this helpful?

launch::async
launch::deferred