每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原 始线程(以main()为入口函数的线程)同时运行。如同main()函数执行完会退出一样,当线程执 行完入口函数后,线程也会退出。
使用C++线程库启动线程,可以归结为构造 std::thread 对象
#include
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())); //1
std::thread my_thread{background_task()}; // 2
**join()**是简单粗暴的等待线程完成或不等待。
#include
#include // 1
void hello() // 2
{
std::cout << "Hello Concurrent World\n"; }
int main()
{
std::thread t(hello); // 3
t.join(); // 4
}
等待新线程结束再结束程序
struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<1000000 ; ++j)
{
do_something(i); // 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
}
使用了 try/catch 块确保访问本地状态的线程退出后,函数才结束。当函数 正常退出时,会执行到②处;当函数执行过程中抛出异常,程序会执行到①处。
try…catch 语句的执行过程是:
- 执行 try 块中的语句,如果执行的过程中没有异常拋出,那么执行完后就执行最后一个 catch 块后面的语句,所有 catch 块中的语句都不会被执行;
- 如果 try 块执行的过程中拋出了异常,那么拋出异常后立即跳转到第一个“异常类型”和拋出的异常类型匹配的 catch 块中执行(称作异常被该 catch 块“捕获”),执行完后再跳转到最后一个 catch 块后面继续执行。
使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互,不会等待这个线程结束。不可能有 std::thread 对象能引用它。C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。
通常称分离线程为守护线程(daemon threads)
【例】使用分离线程去处理其他文档
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);
}
}
}
如果用户选择打开一个新文档,需要启动一个新线程去打开新文档①,并分离线程②。
传参启动线程的方法: 不仅可以向 std::thread 构造函数①传递函数名,还可以传递函数所需的参数(实参)
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]; // 1
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer); // 2
t.detach();
}
std::thread 的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。函数有很有可能会在字面值转化成 std::string 对象之前崩溃
**解决方法:**将字面值转化为 std::string 对象
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024]; // 1
sprintf(buffer, "%i",some_param);
std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬垂指针
t.detach();
}
void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,data); // 2
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); // 1
void oops_again(widget_id w)
{
widget_data data;
std::thread t(update_data_for_widget,w,std::ref(data)); // 2
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); // 1
提供的参数为成员函数和所属的指针对象。
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函数。
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;
}
【函数返回 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_): // 1
t(std::move(t_))
{
if(!t.joinable()) // 2
throw std::logic_error(“No thread”);
}
~scoped_thread()
{
t.join(); // 3
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func; // 定义在2.1中
void f()
{
int some_local_state;
scoped_thread t(std::thread(func(some_local_state))); // 4
do_something_in_current_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)); // 对每个线程 调用join()
}
函数模板std :: mem_fn生成指向成员的指针的包装对象,该对象可以存储,复制和调用指向成员的指针。
调用std ::mem_fn时,可以使用对象的引用和指针(包括智能指针)。
线程标识类型为 std::thread::id,可以通过两种方式进行检索:
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();
}