线程管理的基础
我们知道,每个程序至少有一个线程:执行main
()函数的线程,其余线程有各自的入口函数。线程与原始线程同时运行。
启动线程
线程会在std::thread
对象创建的时候启动。其实使用C++线程库启动线程,可以归结为构造std::thread
对象。
#include<thread>
using namespace std;
void do_some_work();
thread my_thread(do_some_work);
如同大多数C++标准库一样,std::thread
可以用可调用类型构造,将带由于函数调用符类型的实例传入std::thread
类中,替换默认的构造函数。
#include<thread>
using namespace std;
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
thread my_thread(f);
这段代码中,提供的函数对象会复制到新线程的存储空间当中。拷贝而成的函数对象的执行和调用都在新线程的内存空间中进行。函数对象的副本应与原始函数对象保持一致。
如果你传递了一个临时变量,而不是一个命名的变量;C++斌一起会将其解析为函数声明,而不是类型对象的定义。
std::thread my_thread(background_task());
这一句话相当于声明了一个名为my_thread
的函数。
启动了线程,你需要明确的是要等待线程结束还是让其自主分离。
如果不等待线程结束,就必须保重线程结束前,可访问的数据的有效性。
struct func
{
int& i;
func(int& i_): i(i_){
}
void operator()()
{
for(unsigned j = 0; j < 1000000; j++)
{
do_something(i); //悬空引用
}
}
};
void oops()
{
int some_local_state = 0;
func my_func(some_local_state);
thread my_thread(my_func);
my_thread.detach(); //线程分离
}
当主线程结束的时候,如果子线程还在运行,它就会去调用do_something(i)
,函数,访问已经销毁的变量。
处理这种问题的常规方法:将数据复制到线程中,而非复制到共享数据中。
如果一个对象作为线程函数,这个对象就会复制到线程中,而后原始对象就会立即销毁。
等待线程完成
如果需要等待线程,就需要std::thread
对象中的join()
方法。同时,调用join()
方法,还可以清理了线程相关的存储部分。这意味着,一个线程只能使用一次join()
方法。
特殊情况下的等待
考虑一个情况,当线程运行之后产生异常,在join()
调用之前抛出,就意味着这次调用会被跳过。所以需要在异常处理过程中调用join()
。
struct func;
void f()
{
int some_local_state = 0;
func my_func(some_local_state);
thread t(my_func);
try
{
do_something_in_current_thread();
}
catch(...)
{
t.join();
throw;
}
t.join();
}
另外一种就是用面向对象的思想:封装成类。
class thread_guard
{
thread& t;
public:
explicit thread_guard(thread& t_);t(t_){
}
~thread_guard()
{
if(t.joinable())
{
t.join();
}
}
};
struct func;
void f()
{
int some_local_state = 0;
func my_func(some_local_state);
thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
}
当然,如果不想等待线程结束,可以分离线程,从而避免异常安全问题。
后台运行线程
使用detach()会让线程在后台运行,这就意味着主线程不能与之直接交互。如果线程分离,那么就不可能有std::thread
对象能引用它。通常也称分离线程为守护线程。
守护线程就是没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行。
thread t(do_background_work);
t.detach();
assert(!t.joinable());
下面就是一个让一个文字处理应用同时编辑多个文档的逻辑代码:
void edit_document(string const& filename)
{
open_document_and_display_gui(filename);
while(!done_editing())
{
user_command cmd = get_user_input();
if(cmd.type == open_new_document)
{
string const new_name = get_filename_from_user();
thread t(edit_document,new_name); //不仅可传递调用寒暑表 ,还可传递参数
t.detach();
}
else
{
process_user_input(cmd);
}
}
}
向线程函数传递参数
在传递参数的过程中,默认参数要拷贝到线程独立内存中,即使参数是引用形式,也可以在新线程中进行访问。
void f(int i,string const& s);
thread t(f, 3, "hello");
在这里传递的是一个字符串的字面值。之后,在线程的上下文中完成const char*
到string
对象的转换。
当动态指针作为阐述传递给线程的时候,会出现崩溃!
void f(int i, string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer,"%i",some_param);
thread t(f, 3, buffer);
t.detach();
}
这时函数很有可能在字面值转化成string
对象之前崩溃(oops
函数结束,栈帧回退)。
解决方案就是在传递到thread
构造函数之前就将字面值转换成string
对象。
但是!还有问题:
期望传递一个引用,但整个对象被复制了。当线程更新一个引用传递的数据结构是,这种情况就可能发生。
void update_date_for_widget(widget_id w, widget_date& date);
void opps_again(widget_id w)
{
widget_data data;
thread t(update_date_for_widget, w, date);
display_status();
t.join();
process_widget_data(data);
}
在这段代码中,构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。传递给函数的参数是data
变量内部拷贝的引用,而非data本身的引用。
解决方案是std::ref
,将参数转换为引用的形式:
thread t(up_date_data_for_widget, w, ref(data));
线程传递成员方法:
class X { public: void do_lengthy_work(); }; X my_x; thread t(&X::do_lengthy_work, &my_x);
在这里,新线程将类方法作为线程函数。
有趣的是,虽然线程中传递的对象不能拷贝,但是可以用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));
转移线程所有权
刚刚我们提到了move()
,这个其实也可以转移线程之间。
void some_function();
void some_other_function();
thread t1(some_function);
thread t2 = move(t1); //t1的所有权转移给了t2,这时就由t2执行some_function
t1 = thread(some_other_function); //因为是临时变量,所以move()移动 操作会隐式调用
thread
支持移动,就意味着线程的所有权可以在函数外进行转移。
thread f()
{
void some_function();
return thread(some_function);
}
thread g()
{
void some_other_function(int);
thread t(some_other_function, 42);
return t;
}
当所有权可以在函数内部传递,就允许thread
实例作为参数进行传递。
void f(thread t);
void g()
{
void some_function();
f(thread(some_function));
thread t(some_function);
f(move(t));
}
当然,也可以把线程对象放到容器里面:
void do_wrok(unsigned id);
void f()
{
vector<thread> threads;
for(unsigned i = 0; i < 20 ; ++i)
{
threads.push_back(thread(do_work, i)); //产生线程
}
for_each(threads.begin(),threads.end(),mem_fn(&thread::join())); //对每个线程调用join()
}
运行时决定线程的数量
std::thread::hardware_concurrency()
方法将返回能同时并发在一个程序中的线程数量。
在多核系统中,返回值可以使CPU的核芯数量。
在这里需要注意的是:启动线程数必须比num_threads少一个,因为已经存在了主线程了。
识别线程
线程主要通过线程id来进行识别:std::thread::id
可以通过调用std::thread::get_id()
来获得线程id。如果std::thread
对象没有和任何线程相关联,get_id()
将返回std::thread::type
默认构造值。
如果两个线程的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();
}
参考文献
[1]C++并发编程.Anthony Wilaiams