第1章 你好,C++的并发世界!
本章主要内容
- 何谓并发和多线程
- 应用程序为什么要使用并发和多线程
- C++的并发史
- 一个简单的C++多线程程序
1.1 何谓并发
最简单和最基本的并发,是指两个或更多独立的活动同时发生。
1.1.1 计算机系统中的并发
目录
1.2.1 为了分离关注点1.2.2 为了性能1.2.3 什么时候不使用并发
并发的两种方式:双核机器的真正并行 Vs. 单核机器的任务切换
1.1.2 并发的途径
- 多进程并发
- 多线程并发
1.2 为什么使用并发?
- 关注点分离(SOC)
- 性能
1.2.1 为了分离关注点
1.2.2 为了性能
1.2.3 什么时候不使用并发
知道何时不使用并发与知道何时使用它一样重要。基本上,不使用并发的唯一原因就是,收益比不上成本。使用并发的代码在很多情况下难以理解,因此编写和维护的多线程代码就会产生直接的脑力成本,同时额外的复杂性也可能引起更多的错误。除非潜在的性能增益足够大或关注点分离地足够清晰,能抵消所需的额外的开发时间以及与维护多线程代码相关的额外成本(代码正确的前提下);否则,别用并发。
1.3 C++中使的并发和多线程
1.4 开始入门
1.4.1 你好,并发世界
一个简单的多线程程序
清单1.1
#include<iostream>
#include<thread>
void hello()
{
std::count<<"Hello xizi,welcome to Concurrent World\n"
}
int main
{
std::thread t(hello);
t.join();
}
1.5 小结
本章中,提及了并发与多线程的含义,以及在你的应用程序中为什么你会选择使用(或不使用)它。还提及了多线程在C++中的发展历程,从1998标准中完全缺乏支持,经历了各种平台相关的扩展,再到新的C++11标准中具有合适的多线程支持。芯片制造商选择了以多核心的形式,使得更多任务可以同时执行的方式来增加处理能力,而不是增加单个核心的执行速度。在这个趋势下,C++多线程来的正是时候,它使得程序员们可以利用新的CPU,带来的更加
强大的硬件并发。
第2章 线程管理
本章主要内容
- 启动新线程
- 等待线程与分离线程
- 线程唯一标识符
2.1 线程管理的基础
每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原始线程(以main()为入口函数的线程)同时运行。如同main()函数执行完会退出一样,当线程执行完入口函数后,线程也会退出。在为一个线程创建了一个 std::thread 对象后,需要等待这个线程结束;不过,线程需要先进行启动。下面就来启动线程。
2.1.1 启动线程
构造 std::thread 对象:
- 方法
void do_some_work();
std::thread my_thread(do_some_work);
- 类
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
命名函数对象:
background_task f;
std::thread my_thread(f);
当把函数对象传入到线程构造函数中时,如果你传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。
例如:
std::thread my_thread((background_task());
这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个 std::thread 对象的函数,而非启动了一个线程。
使用在前面命名函数对象的方式,或使用多组括号①,或使用新统一的初始化语法②,可以避免这个问题。
如下所示:
std::thread my_thread((background_task())); // 1
std::thread my_thread{background_task()}; // 2
使用lambda表达式:
std::thread my_thread([]{
do_something();
do_something_else();
});
清单2.1 函数已经结束,线程依旧访问局部变量
struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i); // 1. 潜在访问隐患:悬空引用
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); // 2. 不等待线程结束
} // 3. 新线程可能还在运行
这个例子中,已经决定不等待线程结束(使用了detach()②),所以当oops()函数执行完成时③,新线程中的函数可能还在运行。如果线程还在运行,它就会去调用do_something(i)函数①,这时就会访问已经销毁的变量。
2.1.2 等待线程完成
如果需要等待线程,相关的 std::tread 实例需要使用join()。
2.1.3 特殊情况下的等待
当程序可能产生异常时的处理:
清单 2.2 等待线程完成
struct func; // 定义在清单2.1中
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
try
{
do_something_in_current_thread();
}
catch(...)
{
t.join(); // 1
throw;
}
t.join(); // 2
}
清单 2.3 使用RAII等待线程完成:
class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_):
t(t_)
{}
~thread_guard()
{
if(t.joinable()) // 1
{
t.join(); // 2
}
}
thread_guard(thread_guard const&)=delete; // 3
thread_guard& operator=(thread_guard const&)=delete;
};
struct func; // 定义在清单2.1中
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
} // 4
2.1.4 后台运行线程
使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有 std::thread 对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。
通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,且没有任何用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,"发后即忘"(fire and forget)的任务就使用到线程的这种方式。
清单2.4 使用分离线程去处理其他文档
void edit_document(std::string const& filename)
{
open_document_and_display_gui(filename);
while(!done_editing())
{
user_command cmd=get_user_input();
if(cmd.type==open_new_document)
{
std::string const new_name=get_filename_from_user();
std::thread t(edit_document,new_name); // 1
t.detach(); // 2
}
else
{
process_user_input(cmd);
}
}
}