C++11 引入的一个标准库类std::thread
,用于创建和管理线程。它提供了一种方便的方式来执行并发任务,使得多线程编程变得更加简单和安全。
线程管理的基础
每个程序运行时都会有一个线程,即执行main函数的线程。
启动线程
通过std::thread对象来创建线程,线程在创建时启动,可以选择阻塞join
或者分离detach
执行。
这是一段启动线程的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include "bits/stdc++.h" #include <thread>
using namespace std;
void hello() { cout << "Hello Concurrent World" << endl; }
int main (int argc, char *argv[]) { thread t{hello}; t.join(); return 0; }
|
如果线程对象t在析构时还未选择阻塞join
或者分离detach
,std::thread
的析构函数会调用std::terminate()
,会报以下错误:
1 2
| terminate called without an active exception Aborted
|
如果主线程结束时子线程还未结束,那么子线程也会被迫结束,例如以下例子是没有输出的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include "bits/stdc++.h" #include <thread>
using namespace std;
void hello() { this_thread::sleep_for(chrono::seconds(1)); cout << "Hello Concurrent World" << endl; }
int main (int argc, char *argv[]) { thread t{hello}; t.detach(); return 0; }
|
线程的分离
使用detach
会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有std::thread
对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。
线程分离detach
之后,只要主线程还在运行,线程就不会终止,而是会继续执行直到正常退出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include "bits/stdc++.h" #include <thread>
using namespace std;
void hello() { this_thread::sleep_for(chrono::seconds(1)); cout << "Hello Concurrent World" << endl; }
void test_detach() { thread t{hello}; t.detach(); }
int main (int argc, char *argv[]) { test_detach(); this_thread::sleep_for(chrono::seconds(2)); return 0; }
|
线程的分离需要特别注意变量的生命周期,避免线程依旧访问已经析构的变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include "bits/stdc++.h" #include <thread>
using namespace std;
struct func { int& i; func(int& i_) : i(i_) {} void operator() () { for (unsigned j=0 ; j<1000000 ; ++j) { i++; cout << "i = " << i << endl; } } };
void oops() { int some_local_state=0; func my_func(some_local_state); std::thread my_thread(my_func); my_thread.detach(); }
int main (int argc, char *argv[]) { oops(); this_thread::sleep_for(10ms); return 0; }
|
上面这个例子可以看出,多线程对内存管理有着更高的要求,在使用引用传递和指针传递时需要格外谨慎。处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。
线程的等待
一般来说,线程的等待要比线程的分离更加难以处理,因为线程的分离可以在线程创建时就执行,而线程的等待需要精心挑选一个位置。因为调用join
会让当前线程进入阻塞,无法执行其他任务,导致多线程的收益减弱。
除此之外,还可以使用std::thread::joinable
来判断线程是否可以调用join
,如果线程已经被join
、detach或者已经结束了,则返回False
线程管理实践
一种方式是使用“资源获取即初始化方式”(RAII,Resource Acquisition Is Initialization),并且提供一个类,在析构函数中使用**join()**,如同下面清单中的代码。看它如何简化f()函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include "bits/stdc++.h" #include <thread>
using namespace std;
struct func { int& i; func(int& i_) : i(i_) {} void operator() () { for (unsigned j=0 ; j<1000000 ; ++j) { i++; cout << "i = " << i << endl; } } };
class thread_guard { std::thread& t; public: explicit thread_guard(std::thread& t_): t(t_) {} ~thread_guard() { if(t.joinable()) { t.join(); } } thread_guard(thread_guard const&)=delete; thread_guard& operator=(thread_guard const&)=delete; };
void oops() { int some_local_state=0; func my_func(some_local_state); std::thread my_thread(my_func); thread_guard my_guard(my_thread); }
int main (int argc, char *argv[]) { oops(); this_thread::sleep_for(10ms); return 0; }
|
当执行完oops函数时,会对my_guard对象进行析构,调用my_guard的析构函数。析构函数判断线程是否可join,并等待线程结束。
拷贝构造函数和拷贝赋值操作被标记为=delete
,是为了不让编译器自动生成它们。直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。