Simplicity is the Ultimate Sophistication --Leonardo da Vinci.
Overview
无论是服务端编程, 云计算. 并行程序设计已经成为了一个合格的程序员必须掌握的技能之一. 而C++作为一门偏底层的高级编程语言, 从C++入手学习多线程更有助于初学者打好坚实的基础. 自从C++ 11发布以来, 多线程开发变得标准化了, 这意味着C++多线程编程代码可以不再依赖与特定的平台即POSIX 和 windows API可以沿用一套代码, 极大的促进了编程效率.
首先先来看C++到底提供了哪些供与并发编程的组件
可见, C++对多线程编程的支持越来越多, 但核心部分和其他多线程编程是类似的, 咱们先来回顾以下基础也是核心的概念:
Thread/process
之前面试经常被问及进程和线程有什么不同, 一般标答会这样答: 进程是系统进行资源分配和调度的一个独立单位, 线程CPU调度和分派的基本单位. 之后列举不同点, 如有共享地址空间的区别, 占用文件描述符等资源区别等等, 最后在提一下两者的从属关系. 其实这些话都可以用对硬件的抽象来概括: 进程是内存的抽象, 线程是CPU的抽象. 是硬件的目的决定进程/线程的本身的目的, 笔者认为从这个角度能更加本质的理解为什么这两个抽象不同.
就C++来说, 线程往往可callable unit有关, 什么是callable unit: 1. 函数指针; 2.函数对象, 也就是通过重载operator()的struct/class; 3. lambda function. 线程的创建者会将callable unit作为线程的构造函数的输入, 并且具有对期生命周期的控制权, 如进行对线程进行join
(当前线程交出控制权新创建的线程, 等待被join的线程结束), detach
(创建出来的线程与当前线程脱离, 成为后台运行的线程). 一旦线程过了生命周期, thread的析构函数会调用std::terminate
终止程序.
Shared Data
在线程调度的时候, 上下文切换是不可预期的, 因为此处的不确定性, 导致了同一段代码, 在多线程的环境操作共享数据的时候, 结果是不可预期的, 也叫做race condition, 那一段会导致race condition的代码被称为critical section. 因此我们需要提供一种协调机制, 使得代码在多线程的环境下, 执行顺序是可预期的.
Mutex
Luckily, mutex是用来在解决race condition的. 当我们在读/写变量的时候, 我们只允许一个线程对当前变量/内存进行读写, 在mutex保护的情况下, 若此时若发生了线程切换, 其他线程访问critical section由于当前已经有其他线程访问, 该线程得不到执行, 因此不会对shared data造成影响, 因此就实现了数据访问线程安全.
C++有不同的锁实现, 用于针对不同的应用场景, 比如运行同一个线程反复加锁, 尝试获取锁, 以及加上时间戳限制等等情况.
Locks/Guard
Locks/Guard主要是为了实现RAII原语, 一般来说, 一个线程加锁之后就需要负责将锁释放, 为了给程序员带来额外的负担, C++引入了包装类std::lock_guard
, std::unique_lock
, std::shared_lock
来应对不同的情况
Conditional variables
Conditional variable是为了解决加锁后其他线程频繁检查锁, 白白耗费CPU时间片的缺陷. 其同步变量通过收发同步的消息来实现, 一个线程是信号发出者, 另一个是接收者; 信号发出者能够根据不同的信号block/wake up接收者.
Tasks
但是根据contional variable进行多个线程之间的消息传递会降低代码的可读性, 增加犯错的几率, C++提供了std::task
机制, 能够自动化的异步处理两个对端协调和沟通的情况. 我们可以将task看成是一个数据通道, 一边发出std::promise
, 一边接受std::future
, 你可以在很多地方发现这种消息传递的影子, 即pipe; 正是这层额外的抽象, 大大方便了我们的编程
目录
回到开始, 本教程希望通过12篇左右简练但不简陋的系列博客介绍modern C++多线程编程相关的api, 内存模型, 和编程范式; 并且对leetcode 上多线程的题目, 对常见的C++多线程的面试编程题进行整理, 目前计划的话题如下:
- 内存模型
- atomic model and lock
- callable object and thread
- Fence
- 编程范式
- thread and related api
- shared memory
- Condition variable
- Task
- case study
- leetcode 多线程习题
- 线程安全的数据结构实现, 以vector为例
- 线程安全的设计模式实现, 以singleton为例
- 拾遗
- C++ 20
- 其他标准库组件: time lib
通该系列博客笔者觉得能够给读者们打好C++多线程的基础.
ps: 如果你觉得本文对你有帮助, 并且对之后的内容有所期待, 希望能获得你的赞, 欢迎指出任何错误与我理解不到位的地方, 期待和大家共同学习进步.
参考/推荐资料
该系列主要参考以下资料:
- https://www.modernescpp.com/index.php
- https://www.educative.io/courses/modern-cpp-concurrency-in-practice-get-the-most-out-of-any-machine
- C++ Concurrency in Action 2nd
- The C++ Standard Library, 2nd Edition