并发编程作为 C++11 系列的一个重大更新部分,值得我们去探究,并应用其提升程序的性能。本系列参考了其他一些文章,对 C++11 并发编程的一些要点进行了总结,并给出一些示例。
引言
C++ 的多线程一直为之诟病,但有了 C++11 的 std::thread 以后,你可以在语言层面编写多线程程序了,直接的好处就是多线程程序的可移植性得到了很大的提高,所以作为一名 C++ 程序员,熟悉 C++11 的多线程编程方式还是很有益处的。
本文参考了这个系列博文:C++11 并发指南系列,整理和排版,仅供自己记录参考。
并发和并行
首先介绍一下并发的概念,很多时候容易和并行混淆。看一下定义:
如果某个系统支持两个或者多个动作(Action)同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。
总结来说就是,并发是程序编写的过程中是否有多个动作存在,而并行是实际执行过程中是否有多个动作执行。
与 C++11 多线程相关的头文件
首先熟悉一下 C++11 的多线程模块的头文件吧。C++11 新标准中引入了多个头文件来支持多线程编程,他们分别是 <atomic>,<thread>,<mutex>,<condition_variable> 和 <future>。
<atomic>:该头文件用于原子操作,主要声明了两个类,std::atomic和std::atomic_flag,另外还声明了一套 C 风格的原子类型和与 C 兼容的原子操作的函数。<thread>:该头文件用于线程操作,主要声明了std::thread类,另外std::this_thread命名空间也在该头文件中,包含一些线程的操作函数。<mutex>:该头文件用于互斥量操作,主要声明了与互斥量相关的类,包括std::mutex系列类,std::lock_guard,std::unique_lock,以及其他的类型和函数。<condition_variable>:该头文件用于条件变量操作,主要声明了与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。<future>:该头文件用于异步调用操作,主要声明了std::promise,std::package_task两个Provider类,以及std::future和std::shared_future两个Future类,另外还有一些与之相关的类型和函数,std::async()函数就声明在此头文件中。
C++11 多线程的 Hello World
Talk is cheap.
1 |
|
输出:1
2print in new thread: Hello World!
print in main thread: Changed
可以看到,我们创建了一个线程,然后传入一个 std::string 的引用,因为函数声明的参数是一个引用,所以在这里,我们必须通过 std::ref 将引用传进去,具体可以参考 C++11 的 std::ref 用法。在子线程中我们改变了这个字符串,然后调用 join() 等待子线程完成,在主线程中打印可以看到字符串已经被改变。这就是线程的基本用法。
thread 详解
首先看一下 std::thread 的构造方式:
| default (1) | thread() noexcept; |
| initialization (2) | template |
| copy [deleted] (3) | thread (const thread&) = delete; |
| move (4) | thread (thread&& x) noexcept; |
- 默认构造函数,创建一个空的 thread 执行对象。
- 初始化构造函数,创建一个 thread 对象,该 thread 对象可被 joinable,新产生的线程会调用
fn函数,该函数的参数由args给出。 - 拷贝构造函数(被禁用),意味着 thread 不可被拷贝构造。
- move 构造函数,move 构造函数,调用成功之后
x不代表任何 thread 执行对象。
线程对象可以被 move,但是不能被拷贝。而对于 move 赋值操作,如果当前对象不可 joinable,需要传递一个右值引用给 move 赋值操作;如果当前对象可被 joinable,则 terminate() 报错。关于线程是否 joinable,可以通过调用 joinable() 来获得,更多关于 joinable 的资料,可以参考 std::thread::joinable
注意:可被 joinable 的 thread 对象必须在他们销毁之前被主线程 join 或者将其设置为 detached。
用法示例:
1 |
|
输出:1
2
3
4
5
6
7
8
9
10
11Thread 2 executing
Thread 1 executing
Thread 2 executing
Thread 1 executing
Thread 2 executing
Thread 1 executing
Thread 2 executing
Thread 1 executing
Thread 2 executing
Thread 1 executing
Final value of n is 5
除了 join,detach,joinable 之外,std::thread 头文件还在 std::this_thread 命名空间下提供了一些辅助函数:
get_id: 返回当前线程的 idyield: 告知调度器运行其他线程,可用于当前处于繁忙的等待状态sleep_for:给定时长,阻塞当前线程sleep_until:阻塞当前线程至给定时间点
其中 yield 是一个特殊的“线程睡眠”函数:
std::this_thread::yield()是将当前线程所抢到的 CPU ”时间片A”让渡给其他线程(其他线程会争抢”时间片A”,注意。此时”当前线程”不参与争抢。等到其他线程使用完”时间片A”后,再由操作系统调度,当前线程再和其他线程一起开始抢 CPU 时间片。- 如果将
std::this_thread::yield()上述语句修改为:return;,则将未使用完的 CPU ”时间片A”还给操作系统,再由操作系统调度,当前线程和其他线程一起开始抢CPU时间片。
因此 yield 使用的场景就是当当前线程运行条件不满足时调用,避免一个线程频繁与其他线程争抢 CPU 时间片, 从而导致多线程处理性能下降。sleep_for 也是让线程等待,需要等待若干时间。