一、并发概念
1.1 为什么使用并发
分离关注点
- 将相关的代码与无关的代码分离,可以使程序更 容易理解和测试,从而减少出错的可能性。
- 使一些功能区域中的操作需要在同一时刻发生的情况下,使用并发分离不同的功能区域
提高性能
- 将一个单个任务分成几部分,且各自并行运行,从而降低总运行时间。这就是任务并行(task parallelism)
- 在数据方面每个线程在 不同的数据部分上执行相同的操作。数据并行(data parallelism)。
1.2 不使用并发
1.3 C++新标准中的并发
- C++11 : 管理线程、保护共享数据、线程间同步操作,以及低层原子操作
- C++14 : 为并发和并行添加了一个新的互斥量类型,用于保护共享数据
- C++17 : 一开始就添加了一整套的并行算法
二、线程管理基础
每个程序至少有一个线程:执行main() 函数的线程,其余线程有其各自的入口函数。线程与原 始线程(以main()为入口函数的线程)同时运行。如同main()函数执行完会退出一样,当线程执 行完入口函数后,线程也会退出。
2.1 启动线程
使用C++线程库启动线程,可以归结为构造 std::thread 对象
#include <thread>
void do_some_work();
std::thread my_thread(do_some_work);
std::thread 可以用可调用类型构造,将带有函数调用符类型的实例传 入 std::thread 类中,替换默认的构造函数。
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
void operator()() const ()是操作符的名字, ()的操作将被调用,当对象前面用()时.
代码中,提供的函数对象会复制到新线程的存储空间当中。函数对象的执行和调用都在线程的内存空间中进行。函数对象的副本应与原始函数对象保持一致,否则得到的结果会与我们的期望不同。
当把函数对象传入到线程构造函数中时,如果你传递了一个临时变量,而不是一个命名的变 量;C++编译器会将其解析为函数声明,而不是类型对象的定义。
std::thread my_thread(background_task());
这里相当与声明了一个名为my_thread 的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个 std::thread 对象的函数,而非启动了一个线程。
**解决办法:**使用命名函数对象的方式,或使用多组括号①,或使用新统一的初始化语法②
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};
等待线程完成
**join() **是简单粗暴的等待线程完成或不等待。
#include <iostream>
#include <thread>
void hello()
{
std::cout << "Hello Concurrent World\n"; }
int main()
{
std::thread t(hello);
t.join();
}
等待新线程结束再结束程序
struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i);
}
}
};
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();
throw;
}
t.join();
}
使用了 try/catch 块确保访问本地状态的线程退出后,函数才结束。当函数 正常退出时,会执行到②处;当函数执行过程中抛出异常,程序会执行到①处。
try…catch 语句的执行过程是:
- 执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
- 如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个 catch 块后面继续执行。
后台运行线程
使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互,不会等待这个线程结束。不可能有 std::thread 对象能引用它。C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。 通常称分离线程为守护线程(daemon threads)
- 长时间运行:线程的生命周期可能会从某 一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化
- 只能确定线程什么时候结束,**发后即忘(fire and forget)**的任务就使用到线程的这种方式
【例】使用分离线程去处理其他文档
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);
t.detach();
}
else
{
process_user_input(cmd);
}
}
}
如果用户选择打开一个新文档,需要启动一个新线程去打开新文档①,并分离线程②。 传参启动线程的方法: 不仅可以向 std::thread 构造函数①传递函数名,还可以传递函数所需的参数(实参)
2.2 向线程传参
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");
代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个 std::string 对象作为第二个参数,但这里使用的是字符串的字面值,也就是 char const * 类型。之后,在线程的上下文中完成字面值向 std::string 对象的转化。
当指向动态变量的指针作为参数传递给线程的情况
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
std::thread 的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。函数有很有可能会在字面值转化成 std::string 对象之前崩溃 **解决方法:**将字面值转化为 std::string 对象
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,std::string(buffer));
t.detach();
}
使用线程更新一个引用传递的数据结构
void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data);
display_status();
t.join();
process_widget_data(data);
}
虽然update_data_for_widget①的第二个参数期待传入一个引用,但是 std::thread 的构造函数②并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。 **解决方法:**可以使用 std::ref 将参数转换成引用的形式
void update_data_for_widget(widget_id w,widget_data& data);
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,std::ref(data));
display_status();
t.join();
process_widget_data(data);
}
传递一个成员函数指针作为线程函数
class X
{
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x);
提供的参数为成员函数和所属的指针对象。
std::move 转移一个动态对象到一个线程
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object,std::move(p));
std::thread 构造函数中指定std::move(p) ,big_object 对象的所有权就被首先转移到新创 建线程的的内部存储中,之后传递给process_big_object 函数。
2.3 转移线程所有权
std::thread 可移动,但不可拷贝。 这就说明执行线程的所有权可以在 std::thread 实例中移动
void f1()
{
cout << "f1" << endl;
}
void f2()
{
cout << "f2" << endl;
}
int main()
{
std::thread t1(f1);
cout << "move t1 to t2" << endl;
std::thread t2 = std::move(t1);
cout << "create new thread" << endl;
t1 = std::thread(f2);
cout << "create t3" << endl;
std::thread t3;
cout << "move t2 to t3" << endl;
t3 = std::move(t2);
cout << "move t3 to t1" << endl;
t1 = std::move(t3);
return 0;
}
- 创建新线程t1执行f1
- 使用 std::move() 创建t2,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了,执行f1的函数线程与t2关联。
- 创建一个临时的std::thread 对象,移动操作将会隐式的调用。
- t3使用默认构造方式创建,与任何执行线程都没有关联。
- 调用 std::move() 将与t2关联线程的所有权转移到t3中。因为t2是一个命名对象,需要显式的调用 std::move()
- 将与t3关联线程的所有权转移到t1中。t1已经有了一个关联 的线程,所以这里系统直接调用 std::terminate() 终止程序继续运行。
【函数返回 std::thread 对象】 线程的所有权可以在函数外进行转移
std::thread f()
{
void some_function();
return std::thread(some_function);
}
std::thread g()
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
当所有权可以在函数内部传递,就允许 std::thread 实例可作为参数进行传递
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t));
}
f(std::thread(some_function)); f(std::move(t)); 作为参数调用f(移动)
【 scoped_thread的用法】
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_):
t(std::move(t_))
{
if(!t.joinable())
throw std::logic_error(“No thread”);
}
~scoped_thread()
{
t.join();
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func;
void f()
{
int some_local_state;
scoped_thread t(std::thread(func(some_local_state)));
do_something_in_current_thread();
}
- 新线程直接传递到scoped_thread中
scoped_thread 对象析构时会销毁,然后加入到的构造函数创建的线程对象std::thread t 中去- 把检查放在了构造函数中,并且当线程不可加入时,抛出异常
【量产线程】
void do_work(unsigned id); void f()
{
std::vector<std::thread> threads;
for(unsigned i=0; i < 20; ++i)
{
threads.push_back(std::thread(do_work,i));
}
std::for_each(threads.begin(),threads.end(),
std::mem_fn(&std::thread::join));
}
函数模板std :: mem_fn生成指向成员的指针的包装对象,该对象可以存储,复制和调用指向成员的指针。 调用std ::mem_fn时,可以使用对象的引用和指针(包括智能指针)。
2.4 标识线程
线程标识类型为 std::thread::id ,可以通过两种方式进行检索:
- 可以通过调 用 std::thread 对象的成员函数
get_id() 来直接获取 - 当前线程中调用
std::this_thread::get_id() (这个函数定义在 头文件中)也可以 获得线程标识。
std::thread::id 对象可以自由的拷贝和对比,因为标识符就可以复用。如果id相同就是相同线程 std::thread::id 类型对象提供相当丰富的对比操作:
- 提供为不同的值进行排序
- 实例常用作检测线程是否需要进行一些操作
std::thread::id master_thread;
void some_core_part_of_algorithm()
{
if(std::this_thread::get_id()==master_thread)
{
do_master_thread_work();
}
do_common_work();
}
|