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章 并行和并发

启动和停止线程

C++11中添加了std::thread类,并能使用简洁的方式能够对线程进行启动或停止,线程相关的东西都包含在STL中,并不需要额外的库或是操作系统的实现来对其进行支持。

本节中,我们将实现一个程序对线程进行启动和停止。如果是第一次使用线程的话,就需要了解一些细节。

How to do it...

我们将会使用多线程进行编程,并且会了解到,当程序的某些部分使用多线程时,代码会如何进行操作:

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

    #include <iostream>
    #include <thread>
    
    using namespace std;
    using namespace chrono_literals;
  2. 启动一个线程时,我们需要告诉代码如何执行。所以,先来定义一个函数,这个函数会在线程中执行。这个函数可接受一个参数i,可以看作为线程的ID,这样就可以了解打印输出对应的是哪个线程。另外,我们使用线程ID来控制线程休眠的时间,避免多个线程在同时执行cout。如果出现了同时打印的情况,那就会影响到输出。本章的另一个章节会来详述这个问题:

    static void thread_with_param(int i)
    {
        this_thread::sleep_for(1ms * i);
    
        cout << "Hello from thread " << i << '\n';
    
        this_thread::sleep_for(1s * i);
    
        cout << "Bye from thread " << i << '\n';
    }
  3. 主函数中,会先了解在所使用的系统中能够同时运行多少个线程,使用std::thread::hardware_concurrency进行确定。这个数值通常依赖于机器上有多少个核,或是STL实现中支持多少个核。这也就意味着,对于不同机器,这个函数会返回不同的值:

    int main()
    {
        cout << thread::hardware_concurrency()
            << " concurrent threads are supported.\n";
  4. 现在让我们来启动线程,每个线程的ID是不一样的,这里我们启动三个线程。我们使用实例化线程的代码行为thread t {f, x},这就等于在新线程中调用f(x)。这样,在不同的线程中就可以给于thread_with_param函数不同的参数:

        thread t1 {thread_with_param, 1};
        thread t2 {thread_with_param, 2};
        thread t3 {thread_with_param, 3};
  5. 当启动线程后,我们就需要在其完成其工作后将线程进行终止,使用join函数来停止线程。调用join将会阻塞调用线程,直至对应的线程终止为止:

        t1.join();
        t2.join();
  6. 另一种方式终止的方式是分离。如果不以join或detach的方式进行终止,那么程序只有在thread对象析构时才会终止。通过调用detech,我们将告诉3号线程,即使主线程终止了,你也可以继续运行:

        t3.detach();
  7. 主函数结束前将打印一段信息:

        cout << "Threads joined.\n";
    }
  8. 编译并运行程序,就会得到如下的输出。我们可以看到我们的机器上有8个CPU核。然后,我们可以看到每个线程中打印出的hello讯息,但是在主线程最后,我们只对两个线程使用join。第3个线程等待了3秒,但是再主线程结束的时候,其只完成了2秒的等待。这样,我们就没有办法看到线程3的结束信息,因为主函数在结束之后,我们就没有任何机会将其进行杀死了:

    $ ./threads
    8 concurrent threads are supported.
    Hello from thread 1
    Hello from thread 2
    Hello from thread 3
    Bye from thread 1
    Bye from thread 2
    Threads joined.

How it works...

启动和停止线程其实没有什么困难的。多线程编程的难点在于,如何让线程在一起工作(共享资源、互相等待,等等)。

为了启动一个线程,我们首先定义一些执行函数。没有特定的规定,普通的函数就可以。我们来看一个简化的例子,启动线程并等待线程结束:

void f(int i) { cout << i << '\n'; }

int main()
{
    thread t {f, 123};
    t.join();
}

std::thread的构造函数允许传入一个函数指针或一个可调用的对象,通过这个参数,我们就可以对函数进行调用。当然,我们也可以使用没有任何参数的函数。

如果系统中有多个CPU核,那么线程就可以并行或并发的运行。并行与并发之间有什么区别呢?当计算机只有一个CPU核时,也可以有很多线程并行,但就不可能是并发的了,因为在单核CPU上,每个时间片上只有一个线程在执行。线程在单核上交错着运行,当一个时间片结束后,会对下一个线程进行执行(不过对于使用者来说,看起来就像是同时在运行)。如果线程间可以不去分享一个CPU和,那么这些线程就是并发运行。其实并发才是真正的同时运行。

这样,以下几点是我们绝对无法控制的:

  • 共享一个CPU核时,无法控制线程交替运行的顺序。

  • 线程也是有优先级的,优先级会影响线程执行的顺序。

  • 实际上线程是分布在所有CPU核上的,当然操作系统也可以将线程绑定在一个核上。这也就意味着所有的线程可以运行在单核上,也可以运行在具有100个CPU核的机器上。

大多数操作系统都会提供对多线程编程提供一些可能性,不过这些特性并没包含在STL中。

在启动和停止线程的时候,告诉他们要做什么样的工作,并且什么时候线程停止工作。对于大多数应用来说就够用了。本节中,我们启动的3个线程。之后,对其中两个进行了join,另一个进行detach。让我们使用一个简单的图来总结一下本节的代码:

这幅图的顺序是自顶向下,你会看到我们将整个程序分成了4个线程。一开始,启动了额外3个线程来完成一些事情,之后主线程仅等待其他线程的结束。

线程结束对函数的执行后,会从函数中返回。标准库会进行相关的操作,将线程从操作系统的中删除,或用其他方式销毁,所以这里就不用操心了。

我们需要关心的就是join。当对线程对象调用函数x.join()时,其会让调用线程休眠,直至x线程返回。如果线程处于一个无限循环中,就意味着程序无法终止。如果想要一个线程继续存活,并持续到自己结束的时候,那就可以调用x.detach()。之后,就不会在等这个线程了。不管我们怎么做——都必须join或detach线程。如果不想使用这两种方式,可以在线程对象的析构函数中调用std::terminate(),这个函数会让程序“突然死亡”。

主函数返回时,整个程序也就结束了。不过,第3个线程t3还在等待,并将对应的信息打印到终端。操作系统才不会在乎——会直接将我们的程序终止,并不管是否有线程还未结束。要怎么解决这个这个问题,就是开发者要考虑的事情了。

Previous让程序在特定时间休眠Next打造异常安全的共享锁——std::unique_lock和std::shared_lock

Last updated 6 years ago

Was this helpful?