
一、简介
C++11 加入对多线程的支持
并发:是指两个或更多独立的活动同时发生
多进程并发、多线程并发
为什么使用并发:关注点分离和性能
初始线程始于 main(),而新线程始于入口函数
eclipse 配置:C++11、多线程
二、线程管理
1、基础
1)启动
函数名:
1. #include <thread>
2. void work();
3. thread t(work);
函数对象:
1. class task
2. {
3. public:
4. void operator()() const
5. {
6. do_something();
7. do_something_else();
8. }
9. };
10. task work;
11. thread t(work);
1. thread t((task())); //多组括号
2. thread t{task()}; //新的初始化语法
3. thread t([]{ //lambda 表达式
4. do_something();
5. do_something_else();
6. });
启动了线程,必须在 thread 对象销毁之前做出决定——加入 joined 或分离 detached
如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性,这种情况很可能
发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引
用。如下所示,函数已经结束,线程依旧访问局部变量。
1. struct func

2. {
3. int& value;
4. func(int& temp) : value(temp) {}
5. void operator() ()
6. {
7. for (int j = 0 ; j < 1000000 ; ++j)
8. {
9. do_something(value); // 1. 潜在访问隐患:悬空引用,oops 函数退出后已无法继续执行
10. }
11. }
12. };
13. void oops()
14. {
15. int local = 0;
16. func my_func(local);
17. thread my_thread(my_func);
18. my_thread.detach(); // 2. 不等待线程结束
19. } // 3. 新线程可能还在运行
只能对一个线程使用一次 join(),一旦已经使用过 join(),thread 对象就不能再次加入了,
当对其使用 joinable()时,将返回否(false)
2)join() 等待线程完成
如果打算等待对应线程,则需要细心挑选调用 join()的位置。当在线程运行之后产生异
常,在 join()调用之前抛出,就意味着这次调用会被跳过。try/catch/throw 无异常的情况下
使用 join(),需要在异常处理过程中调用 join(),见下面程序
1. void f()
2. {
3. int local=0;
4. func my_func(local);
5. thread t(my_func);
6. try
7. {
8. do_something();
9. }
10. catch(...)
11. {
12. t.join(); // 1
13. throw;
14. }
15. t.join(); // 2
16. }
(析构函数中使用 join(),见下面程序)当线程执行到④处时,局部对象就要被逆序销毁了。
因此,thread_guard 对象是第一个被销毁的,这时线程在 thread_guard 类的析构函数中被加
入②到原始线程中。即使 do_something_in_current_thread 抛出一个异常,这个销毁依旧会
发生。
在 thread_guard 类的析构函数中,首先判断线程是否已加入①,如果没有则调用 join()②
进行加入。这很重要,因为 join()只能对给定的对象调用一次,所以对给已加入的线程再次
进行加入操作时,将会导致错误。
拷贝构造函数和拷贝赋值操作被标记为=delete③,是为了不让编译器自动生成它们。
直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声
明,任何尝试给 thread_guard 对象赋值的操作都会引发一个编译错误
1. class thread_guard
2. {
3. thread& t;
4. public:
5. explicit thread_guard(thread& t_): t(t_) {}

6. ~thread_guard()
7. {
8. if(t.joinable()) // 1
9. {
10. t.join(); // 2
11. }
12. }
13. thread_guard(thread_guard const&)=delete; // 3
14. thread_guard& operator=(thread_guard const&)=delete;
15. };
16.
17. void f()
18. {
19. int local=0;
20. func my_func(local);
21. thread t(my_func);
22. thread_guard g(t);
23. do_something();
24. } // 4
3)detach()分离线程,后台运行线程
如果线程分离,那么就不可能有 thread 对象能引用它,分离线程的确在后台运行,所
以分离线程不能被加入。不过 C++运行库保证,当线程退出时,相关资源的能够正确回收,
后台线程的归属和控制 C++运行库都会处理
分离线程即守护线程,分离线程不能被加入
长时间运行,可能会在后台监视文件系统,还有可能对缓存进行清理,或对数据结构进
行优化
thread 对象使用 joinable()返回的是 true,就可以使用 detach()
例如:文字处理应用同时编辑多个文档,打开一个文档就要启动一个新线程。因为是对
独立的文档进行操作,所以没有必要等待其他线程完成。因此,这里就可以让文档处理窗口
运行在分离的线程上
(如下,使用分离线程去处理其他文档)如果用户选择打开一个新文档,为了迅速打开
文档,需要启动一个新线程去打开新文档①,并分离线程②。与当前线程做出的操作一样,
新线程只不过是打开另一个文件而已。所以,edit_document()函数可以复用,通过传参的形
式打开新的文件。这个例子也展示了传参启动线程的方法:不仅可以向 thread 构造函数①
传递函数名,还可以传递函数所需的参数(实参)
1. void edit_document(string const& filename)
2. {
3. open_document_and_display_gui(filename);
4. while(!done_editing())
5. {
6. user_command cmd=get_user_input();
7. if(cmd.type==open_new_document)
8. {
9. string const new_name=get_filename_from_user();
10. thread t(edit_document, new_name); // 1
11. t.detach(); // 2
12. }
13. else
14. {
15. process_user_input(cmd);
16. }
17. }
18. }

2、向线程函数传递参数
默认:参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进
行访问
1. void f(int v, string const& s);
2. thread t(f, 3, "hello");
代码创建了一个调用 f(3, "hello")的线程。注意,函数 f 需要一个 string 对象作为第二个
参数,但这里使用的是字符串的字面值,也就是 char const *类型。之后,在线程的上下文
中完成字面值向 string 对象的转化
1. void update_data(widget w, widget_data& data); // 1
2. void oops_again(widget w)
3. {
4. widget_data data;
5. thread t(update_data, w, data); // 2
6. display_status();
7. t.join();
8. process_widget_data(data); // 3
9. }
虽然 update_data_for_widget①的第二个参数期待传入一个引用,但是 thread 的构造函
数②并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。当线程
调用 update_data_for_widget 函数时,传递给函数的参数是 data 变量内部拷贝的引用,而
非 data 数据本身的引用。因此,当线程结束时,内部拷贝数据将会在数据更新阶段被销毁,
且
process_widget_data 将会接收到没有修改的 data 变量③
使用 ref()将参数转换成引用的形式:
1. thread t(update_data, w, ref(data));
在这之后,update_data 就会接收到一个 data 变量的引用,而非 data 变量拷贝的引用
新线程将 my_x.do_lengthy_work()作为线程函数,my_x 的地址①作为指针对象提供给函
数。也可以为成员函数提供参数,thread 构造函数的第三个参数就是成员函数的第一个参数,
以此类推
1. class X
2. {
3. public:
4. void do_lengthy_work();
5. };
6. X my_x;
7. thread t(&X::do_lengthy_work, &my_x); // 1
8.
9. class Y
10. {
11. public:
12. void do_lengthy_work(int);
13. };
14. Y my_y;
15. int num(0);
16. thread t(&Y::do_lengthy_work, &my_y, num);
为了更容易(同时也更安全)地使用动态内存,C++11 标准库提供了两种智能指针类型来
管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指的对象。
C++11 标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr 允许多个

指针指向同一个对象;unique_ptr 则"独占"所指向的对象。C++11 标准库还定义了一个名为
weak_ptr 的辅助类,它是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义
在 memory 头文件中
unique_ptr(C++11 中的智能指针),这种类型为动态分配的对象提供内存自动管理机制(类
似垃圾回收)。同一时间内,只允许一个 unique_ptr 对象指向一个给定对象,并且当某个
unique_ptr 对象销毁时,指向的对象也将被删除
转移的时候就需要使用 move()进行显式移动,使用"移动"转移原对象后,就会留下一个
空指针(NULL)
在 thread 的构造函数中指定 move(p),int 对象的所有权就被首先转移到新创建线程的
内部存储中,之后传递给 func 函数
1. void func(unique_ptr<int> input)//input 指向 0x616e70
2. {
3. *input = *input + 10;
4. cout<<"concurrent!"<<*input<<endl;
5. }
6.
7. int main() {
8. unique_ptr<int> p(new int(10));
9. thread t1(func, move(p));//p 指向 0x616e70
10. t1.join();
11. cout << "!!!Hello World!!!" << endl;//p=null
12. return 0;
13. }
标准线程库中 unique_ptr 和 thread 在所属权上有相似的语义
thread 实例会占用一部分资源的所有权,每个实例都管理一个执行线程。thread 所有权
可以在多个实例中互相转移,因为这些实例是可移动(movable)且不可复制(aren't copyable)。
在同一时间点,就能保证只关联一个执行线程;同时,也允许程序员能在不同的对象之间
转移所有权
3、转移线程所有权
执行线程的所有权可以在 thread 实例中移动
当调用 move()创建 t2 后②,t1 的所有权就转移给了 t2。之后,t1 和执行线程已经没有
关联了;执行 some_function 的线程现在与 t2 关联。然后,与一个临时 thread 对象相关的
线程启动了③。为什么不显式调用 move() 转移所有权呢?因为,所有者是一个临时对象—
移动操作将会隐式的调用。t3 使用默认构造方式创建④,与任何执行线程都没有关联。调
用 move()将与 t2 关联线程的所有权转移到 t3 中⑤,显式的调用 move(),是因为 t2 是一个
命名对象。移动操作⑤完成后,t1 与执行 some_other_function 的线程相关联,t2 与任何线
程都无关联,t3 与执行 some_function 的线程相关联
1. void some_func() {}
2. void other_func() {}
3.
4. int main() {
5. thread t1(some_func); //1
6. thread t2 = move(t1); //2
7. t1 = thread(other_func); //3
8. thread t3; //4
9. t3 = move(t2); //5
10. t1 = move(t3); //6, 赋值操作将使程序崩溃
11. return 0;
12. }