线程管理的基础

我们知道,每个程序至少有一个线程:执行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