第2章:线程管理(C++并发编程实战)

2.1线程管理的基础

2.1.1启动线程

使用C++线程库启动线程,可以归纳为构造std::thread对象:

void do_some_work();
std::thread my_thread(do_some_work);

为了让编译器识别std::thread类,这个简单的例子也要包括<thread>头文件。如同大多数C++标准库一样,std::thread可以调用类型构造,将带有函数调用符类型的实例传入std::thread类中,替换默认的构造函数:

class background_task
{
public:
	void operator()() const
	{
		do_something();
		do_something_else();
	}
};

background_task f;
std::thread my_thread(f);

特别注意:当把函数对象传入到线程构造函数中时,如果你传递的是一个临时变量,而不是一个命名的变量,C++编译器会将其解析为函数声明,而不是类型对象的定义

std::thread my_thread(background_task());

这里相当于声明了一个名为my_thread函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了 一个线程。

使用前面命名函数对象的方式,或使用多组括号(1),或使用新统一的初始化语法(2),可以避免这个问题:

std::thread my_thread((background_task())); //(1)
std::thread my_thread{background_task()};	//(2)

(3)使用lambda表达式也能避免这个问题:

std::thread my_thread
(
	[]{do_something();
	   do_something_else();}
);

启动了线程,需要明确是否需要等待线程或者让其自主运行。如果不等待线程,就必须保证线程结束前,可访问的数据是否有效。这就像单线程中,对象销毁之后再去访问,会产生未定义行为。

这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程还持有局部函数变量的指针和引用。

struct func
{
	int& i;
	func(int& i_):i(i_){}
	void operator() ()
	{
		for(size_t 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).新线程可能还在运行
}

此例子决定不再等待线程结束(2),所以oops()函数指向完成时(3),新线程中的函数还可能还在运行。它就会去调用do_something(i)函数(1),这时就会访问已经销毁的变量。就像单线程中——允许在函数完成后继续持有局部变量的指针和引用,会导致悬垂指针问题。

最好的方法是:将数据复制到线程中区,而非共享一个数据。此外还可以通过join()函数来确保线程在函数完成之前结束。

2.1.2等待线程完成

如果需要等待线程,就要使用join(),就可以确保局部变量在线程中完成后,才被销毁。

使用join()是简单粗暴的等待完成,当你需要对等待的中的线程有更加灵活的控制的时:比如看一下某个线程是否结束,或者只等待一段时间(是否超时),你就需要其他机制来完成,比如条件变量和futures。调用join()还清理了线程相关的存储部分,这意味着只能对一个线程使用一次join();一旦已经使用过join(),std::thread对象就不能再次加入了,当其使用joinable()将返回false。

2.1.3特殊情况下的等待

如果在join()之前,抛出异常的时候,避免程序异常终止就需要异常处理:

struct func;
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
}

另一种方法使用RAII(资源获取就初始化),提供一个类,在析构中使用join():

class thread_guard
{
	st::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;

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

当线程执行到(4)的时候,局部对象被逆序销毁。即使do_something_in_current_thread()抛异常,thread_guard的析构函数依然会被调用。

在thread_guard析构函数进行测试,判断线程是否已经加入(1),如果没有调用join()(2)才进行加入,如果给已经join()的线程再次加入操作,就会导致错误。

拷贝构造和拷贝赋值被标记为delete(3),不让编译器自动生成。直接对一个对象进行拷贝或赋值是危险的。

2.1.4后台运行线程

使用detach()会让线程在后台运行,这意味着主线程不能与之产生直接交互。通常称分离线程为守护线程(daemon threads),UNIX中守护线程指,没有任何显示的用户接口,并在后台运行的线程。

这种线程特点是长时间运行;生命周期可能会从某个一个应用起始到结束,可能后在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。分离线程的另外一方面只能确定线程什么时候结束,发后既忘(fire and forget)的任务就是使用线程的这种方式。

对一个文字处理应用同时编辑多个文档。一种内部的处理方式就是让每一个文档对象窗口拥有自己的线程;每个线程运行同样的代码,并隔离不同的窗户处理的数据,打开一个文档就要启动一个线程。因为是独立的文档进行操作,所以没必要等待其他线程完成。可以分离线程:

void edit_document(std::string& 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);
		}
	}
}

如果用户选择打开一个新文档,需要启动一个新线程去打开新文档1,并分离线程2。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值