C++ 11 新标准中, 正式的为该语言引入了多线程概念. 新标准提供了一个线程库 thread, 通过创建一个 thread 对象来管理 C++ 程序中的多线程.
本文简单聊一下 C++ 多线程相关的一些概念及 thread 的基本用法.
0. 并行执行
程序并行执行两个必要条件:
多处理器 (multiple processors)or 多核处理器 (multicore processors)
软件并行
软件并发执行可分为两大类:
多线程并发 (同一个进程的多个线程并行);
多进程并发 (不同进程并行);
对于多线程, 主要关注的是线程间的同步措施, 用于确保线程安全;
对于多进程, 主要关注的是进程间的通信机制, 用于进程间传递消息和数据;
由于 C++ 标准中没有多进程之间通信相关的标准, 这些只能依赖于特定平台的 API. 本文只关注多线程相关.
1. C++ 多线程平台
C++11 之前, Windows 和 Linux 平台分别有各自的多线程标准. 使用 C++ 编写的多线程往往是依赖于特定平台的.
Windows 平台提供用于多线程创建和管理的 win32 API;
Linux 下则有 POSIX 多线程标准, Threads 或 Pthreads 库提供的 API 可以在类 Unix 上运行;
在 C++11 新标准中, 可以简单通过使用 hread 库, 来管理多线程. thread 库可以看做对不同平台多线程 API 的一层包装;
因此使用新标准提供的线程库编写的程序是跨平台的.
2. pthread 或 C++ 11 thread
pthreads 是 Linux 下的 C++ 线程库, 提供了一些线程相关的操作, 比较偏向于底层, 对线程的操作也是比较直接和方便的;
- #include <pthread.h>
- pthread_create (thread, attr, start_routine, arg)
Linux 上对于 pthread 的使用需要连接 pthread 库 (有些编辑器可能需要 -std=c++11):
g++ source.cpp -lpthread -o source.o
尽管网上对 C++ 11 新标准中的 thread 类有很多吐槽, 但是作为 C++ 第一个标准线程库, 还是有一些值得肯定的地方的, 比如跨平台, 使用简单.
而且新标准中可以方便的使用 RAII 来实现 lock 的管理等.
如果你想深入研究一下多线程, 那么 pthread 是一个不错的选择. 如果想要跨平台或者实现一些简单的多线程场景而不过多关注细节, 那么权威的标准库 thread 是不二之选.
总之没有好与坏之分, 适合就好. 可以的话可以都了解一下. 本文主要介绍后者.
3. 先理论后实践
对于多线程相关的学习, 先弄清楚线程相关的一些概念, 是很重要的.
比如线程安全, 线程同步与互斥关系, 线程如何通信, 与进程的关系如何等.
不然实际写多线程程序是会碰到太多的问题, 例如:
程序死锁, 无响应;
执行结果不符合预期;
多线程性能并没有很大提升;
理不清程序执行流程;
不知道怎么调试;
程序运行时好时坏;
光线程安全就有很多理论要了解, 这些光靠调试程序, 根据结果来猜测是不可行的.
关于多线程相关的概念可以参考我之前以 Python 为例介绍线程的博文:
python 多线程与多进程及其区别;
python 多线程同步实例分析;
4. thread 多线程实例
看一下 C++11 使用标准库 thread 创建多线程的例子:
- #include<iostream>
- #include<thread>
- #include<string>
- using namespace std;
- int tstart(const string& tname) {
- cout <<"Thread test!" << tname << endl;
- return 0;
- }
- int main() {
- thread t(tstart, "C++ 11 thread!");
- t.join();
- cout << "Main Function!" << endl;
- }
多线程标准库使用一个 thread 的对象来管理产生的线程. 该例子中线程对象 t 表示新建的线程.
4.1 标准库创建线程的方式
打开 thread 头文件, 可以清楚的看到 thread 提供的构造函数.
默认构造函数 thread() noexcept;
接受函数及其传递参数的构造函数 template <class _Fn, class... _Args, ...> explicit thread(_Fn&& _Fx, _Args&&... _Ax)
move 构造函数 thread(thread&& _Other) noexcept;
拷贝构造函数 thread(const thread&) = delete;
拷贝赋值运算符 thread& operator=(const thread&) = delete;
其中拷贝构造函数和拷贝赋值运算符被禁用, 意味着 std::thread 对象不能够被拷贝和赋值到别的 thread 对象;
默认构造函数构造一个空的 thread 对象, 但是不表示任何线程;
接受参数的构造函数创建一个表示线程的对象, 线程从传入的函数开始执行, 该对象是 joinable 的;
move 构造函数可以看做将一个 thread 对象对线程的控制权限转移到另一个 thread 对象; 执行之后, 传入的 thread 对象不表示任何线程;
- int main()
- {
- int arg = 0;
- std::thread t1; // t1 is not represent a thread
- std::thread t2(func1, arg + 1); // pass to thread by value
- std::thread t3(func2, std::ref(arg)); // pass to thread by reference
- std::thread t4(std::move(t3)); // t4 is now running func2(). t3 is no longer a thread
- //t1.join() Error!
- t2.join();
- //t3.join() Error!
- t4.join();
- }
多数情况下我们使用的是上面第二种创建线程的方式. 下面看一下 join 和 detach.
4.2 join && detach
对于创建的线程, 一般会在其销毁前调用 join 和 detach 函数;
弄清楚这两个函数的调用时机和意义, 以及调用前后线程状态的变化非常重要.
join 会使当前线程阻塞, 直到目标线程执行完毕;
只有处于活动状态线程才能调用 join, 可以通过 joinable() 函数检查;
joinable() == true 表示当前线程是活动线程, 才可以调用 join 函数;
默认构造函数创建的对象是 joinable() == false;
join 只能被调用一次, 之后 joinable 就会变为 false, 表示线程执行完毕;
调用 ternimate() 的线程必须是 joinable() == false;
如果线程不调用 join() 函数, 即使执行完毕也是一个活动线程, 即 joinable() == true, 依然可以调用 join() 函数;
detach 将 thread 对象及其表示的线程分离;
调用 detach 表示 thread 对象和其表示的线程完全分离;
分离之后的线程是不在受约束和管制, 会单独执行, 直到执行完毕释放资源, 可以看做是一个 daemon 线程;
分离之后 thread 对象不再表示任何线程;
分离之后 joinable() == false, 即使还在执行;
join 实例分析:
- int main() {
- thread t(tstart, "C++ 11 thread!");
- cout << t.joinable() << endl;
- if (t.joinable()) t.join();
- //t.detach(); Error
- cout << t.joinable() << endl;
- // t.join(); Error
- cout << "Main Function!" << endl;
- system("pause");
- }
简单来说就是只有处于活动状态的线程才可以调用 join, 调用返回表示线程执行完毕, joinable() == false.
- inline void thread::join()
- { // join thread
- if (!joinable())
- _Throw_Cpp_error(_INVALID_ARGUMENT);
- const bool _Is_null = _Thr_is_null(_Thr); // Avoid Clang -Wparentheses-equality
- ... ...
- }
将上面的 t.join() 换成是 t.detach() 会得到相同的结果.
- void detach()
- { // detach thread
- if (!joinable())
- _Throw_Cpp_error(_INVALID_ARGUMENT);
- _Thrd_detachX(_Thr);
- _Thr_set_null(_Thr);
- }
上面是 thread 文件中对 detach 的定义, 可以看出只有 joinable() == true 的线程, 也就是活动状态的线程才可以调用 detach.
- ~thread() _NOEXCEPT
- { // clean up
- if (joinable())
- _XSTD terminate();
- }
当线程既没有调用 join 也没有调用 detach 的时候, 线程执行完毕 joinable() == true, 那么当 thread 对象被销毁的时候, 会调用 terminate().
4.3 获取线程 ID
线程 ID 是一个线程的标识符, C++ 标准中提供两种方式获取线程 ID;
- thread_obj.get_id();
- std::this_thread::get_id()
有一点需要注意, 就是空 thread 对象, 也就是不表示任何线程的 thread obj 调用 get_id 返回值为 0;
此外当一个线程被 detach 或者 joinable() == false 时, 调用 get_id 的返回结果也为 0.
- cout << t.get_id() << ' ' << this_thread::get_id() << endl;
- //t.detach();
- t.join();
- cout << t.get_id() << ' ' << std::this_thread::get_id() << endl;
4.4 交换 thread 表示的线程
除了上面介绍的 detach 可以分离 thread 对象及其所表示的线程, 或者 move 到别的线程之外, 还可以使用 swap 来交换两个 thread 对象表示的线程.
实例来看一下两个线程的交换.
- int tstart(const string& tname) {
- cout << "Thread test!" << tname << endl;
- return 0;
- }
- int main() {
- thread t1(tstart, "C++ 11 thread_1!");
- thread t2(tstart, "C++ 11 thread_2!");
- cout << "current thread id:" << this_thread::get_id() << endl;
- cout << "before swap:"<< "thread_1 id:" << t1.get_id() << "thread_2 id:" << t2.get_id() << endl;
- t1.swap(t2);
- cout << "after swap:" << "thread_1 id:" << t1.get_id() << "thread_2 id:" << t2.get_id() << endl;
- //t.detach();
- t1.join();
- t2.join();
- }
结果:
- Thread test! C++ 11 thread_1!
- Thread test! C++ 11 thread_2!
- current thread id: 39308
- before swap: thread_1 id: 26240 thread_2 id: 37276
- after swap: thread_1 id: 37276 thread_2 id: 26240
下面是 thread::swap 函数的实现.
- void swap(thread& _Other) _NOEXCEPT
- { // swap with _Other
- _STD swap(_Thr, _Other._Thr);
- }
可以看到交换的过程仅仅是互换了 thread 对象所持有的底层句柄;
关于 C++ 多线程新标准 thread 的基本介绍就到这里了, 看到这里应该有一个简单的认识了.
关于线程安全和管理等高级话题, 后面有空在写文章介绍.
来源: https://www.cnblogs.com/yssjun/p/11533346.html