C++并发编程实战读书笔记——线程管理

并发的两种途径:多个进程,每个进程只有一个线程(multiple single-threaded processes);每个进程有多个线程(multiple threads in a single process)。多个单线程/进程的启动和通信开销要比单一进程多线程间的启动和通信大。若不考虑共享内存可能带来的问题,多线程是主流语言(包括C++)更青睐的并发途径。此外,C++标准并未对进程间通信提供任何原生支持,如果程序使用多进程方式实现,这将会依赖与平台相关的API。(The low overhead associated with launching and communicating between multiple threads within a process compared to launching and communicating between multiple single-threaded processes means that this is the favored approach to concurrency in mainstream languages including C++, despite the potential problems arising from the shared memory.In addition, the C++ Standard doesn’t provide any intrinsic support for communication between processes, so applications that use multiple processes will have to rely on platform-specific APIs to do so.)

Hello World of concurrency in C++

在这里插入图片描述

  1. 管理线程的函数和类在中声明,而保护共享数据的函数和类在其他头文件中声明。
  2. 每个线程都必须有一个初始函数(initial function),新线程的执行从该函数开始。对于应用程序(初始线程,initial thread),这个函数是main()。对于其他线程,可以在std::thread对象的构造函数中指定。在本例中,std::thread对象t拥有函数hello()作为初始函数。
  3. 新线程启动后,初始线程继续执行。如果它不等待新线程结束,它就将自顾自地继续运行到main()的结束(可能发生在新线程运行之前)。
  4. join()成员函数会导致调用线程(在main()中)等待与std::thread对象相关联的线程(即这个例子中的t)。

线程管理

启动线程

线程在std::thread对象创建(为线程指定任务)时启动。最简单的情况下,任务也会很简单,通常是无参无返回的函数。
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++‘s most vexing parse 最令人头痛的语法解析
当把函数对象传入到线程构造函数中时,如果传递临时变量,而不是一个命名的变量,C++编译器会将其解析为函数声明,而不是类型对象的定义。
相当于声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动一个线程。

std::thread my_thread(background_task());

使用多组括号或新同一的初始化语法来避免

std::thread my_thread((background_task()));
std::thread my_thread{background_task()};

可以传递一个lambda表达式给std::thread的构造函数。

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

等待线程or分离线程

必须在std::thread对象销毁之前做出等待或分离线程决定,否则程序将会终止(std::thread的析构函数会调用std::terminate())
等待线程完成(join)
调用join(),会清理线程相关的存储部分,这样std::thread对象将不再与已经完成的线程有任何关联。对于一个线程只能使用一个join(),不可对该线程再次调用join()。当对其使用joinable()会返回false。
如果打算等待对应线程,需要细心挑选调用join()的位置。当在被调用线程运行之后调用线程产生异常,并在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();
		throw;
	}
	t.join();
}

确保线程在函数之前结束-使用RAII资源获取即初始化方式

class thread_guard
{
	std::thread& t;
public:
	explicit thread_guard(std::thread& t_):
	t(t_)
	{}
	~thread_guard()
	{
		if(t.joinable())
		{
			t.join();
		}
	}
	thread_guard(thread_guard const&)=delete;
	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();
}

当函数f执行结束时,局部对象就要被逆序销毁。因此,thread_guard对象g是第一个被销毁的,确保了线程被加入到原始线程中。即使do_something_in_current_thread抛出一个异常,销毁依旧会发生。

后台运行线程(detach)
使用detach()会让线程在后台运行,这意味着主线程不能与之产生直接交互。(不会等待线程结束)如果线程分离,那就不能有std::thread对象能引用它(打破了线程与std::thread对象的联系,即使std::thread对象被销毁,std::terminate()也不会调用),所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源能够正确回收,后台线程的归属和控制C++运行库都会处理。

std::thread t(do_background_work);
t.detach();
assert(!t.joinable());

不能对没有执行线程的std::thread对象使用detach(),当std::thread对象使用t.joinable()返回的是true,才可以使用t.detach()。

向线程函数传递参数

向std::thread构造函数中的可调用对象,或函数传递一个参数很简单。默认情况下参数要拷贝到线程独立内存中,即使参数是引用的形式,新线程可以进行访问。

void f(int i,std::string const& s);
std::thread t(f,3,”hello”);

字符串字面值将在新线程上下文中转换为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();
}

这个例子中,有很大可能性在buffer被转换为std::string之前函数oops已经退出,这导致了未定义行为。解决方法是在将buffer传递给std::thread构造函数之前转换为std::string。

void f(int i,std::string const& s);
void not_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);
}

std::thread构造函数无视函数期待的参数类型,盲目地拷贝已提供的变量。当线程调用update_data_for_widget函数时,传递给函数的参数是data变量内部拷贝的引用,而非数据本身的引用。当线程结束时,内部拷贝数据将会在数据更新阶段被销毁,且process_widget_data接收到的是没有修改的data变量。
可以使用std::ref将参数转换成引用形式,update_data_for_widget会接收到一个data变量的引用,而非一个data变量拷贝的引用。

std::thread t(update_data_for_widget,w,std::ref(data));

可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数。

class X
{
public:
	void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x);

移动:原始对象中的数据转移给另一对象,而转移的这些数据就不再原始对象中保持了。std::unique_ptr就是这样一种类型,这种类型为动态分配的对象提供内存自动管理机制。同一时间,只允许一个std::unique_ptr指向一个给定对象,并且当这个实现销毁时,指向的对象也将被删除。移动构造函数和移动赋值操作符允许一个对象在多个std::unique_ptr实现中传递。使用移动转移原对象后,就会留下一个空指针。移动操作可以将对象转换成可接受的类型,例如:函数参数或函数返回的类型。当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么移动的时候就需要使用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§,big_object对象的所有权就被首先转移到新创建线程的内部存储中,之后传递给process_big_object函数。

转移线程所有权

std::thread每个实例负责管理一个执行线程。执行线程的所有权可以在多个std::thread实例中相互转移,这依赖于std::thread实例的可移动且不可复制性。

void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2=std::move(t1);
t1=std::thread(some_other_function);
std::thread t3;
t3=std::move(t2);
t1=std::move(t3);

新线程开始与t1相关联,当显式使用std::move()chu’a’g创建t2后,t1的所有权就转移给t2。这时,t1和执行线程已经没有关联。临时std::thread对象相关的线程启动,将所有权转移给t1(临时对象的移动操作将会隐式调用,不需显式调用std::move())。t3使用默认构造方式创建,与任何执行线程都没有关联。调用std::move()将与t2关联线程的所有权转移到t3中。最后一个移动操作,将some_function线程的所有权转移给t1。但是,t1已经有一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()终止程序继续运行。
线程所有权可以转移到函数内外

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

void f(std::thread t);
void g()
{
	void some_function();
	f(std::thread(some_function));
	std::thread t(some_function);
	f(std::move(t));
}

当某个对象转移了线程所有权后,它就不能对线程进行加入或分离。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();
}

如果容器是移动敏感的(比如,标准中的std::vector<>),那么这些移动操作同样适用于std::thread对象的容器。

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::thread放入std::vector是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,并且将它们直径加入,可以把它们当做一个组。

运行时决定线程数量

std::thread::hardware_concurrency()返回能同时并发运行在一个程序中的线程的数量。

template<typename Iterator,typename T>
struct accumulate_block
{
	void operator()(Iterator first,Iterator last,T& result)
	{
		result=std::accumulate(first,last,result);
	}
};
template<typename Iterator,typename T>
T parallel_accumulate(Iterator first,Iterator last,T init)
{
	unsigned long const length=std::distance(first,last);
	if(!length)
		return init;
	unsigned long const min_per_thread=25;
	unsigned long const max_threads=(length+min_per_thread-1)/min_per_thread;
	unsigned long const hardware_threads=std::thread::hardware_concurrency();
	unsigned long const num_threads=std::min(hardware_threads!=0?hardware_threads:2,max_threads);	
	unsigned long const block_size=length/num_threads;
	std::vector<T> results(num_threads);
	std::vector<std::thread> threads(num_threads-1);	
	Iterator block_start=first;
	for(unsigned long i=0;i<(num_threads-1);++i)
	{
		Iterator block_end=block_start;
		std::advance(block_end,block_size);
		threads[i]=std::thread(accumulate_block<Iterator,T>(),block_start,block_end,std::ref(results[i]));
		block_start=block_end;
	}
	accumulate_block<Iterator,T>()(block_start,last,results[num_threads-1]);
	std::for_each(threads.begin(),threads.end(),std::mem_fn(&std::thread::join));
	return std::accumulate(results.begin(),results.end(),init);
}

标识线程

线程标识类型是std::thread::id,可以通过两种方式进行检索。第一种,可以通过调用std::thread对象的成员函数get_id()来直接获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回std::thread::type默认构造值(这个值表示无线程)。第二种,当前线程中调用std::this_thread::get_id()(这个函数定义在头文件中)也可获得线程标识符。
线程库不会限制你去检查线程标识是否一样,std::thread::id类型对象提供相当丰富的对比操作。比如,提供为不同的值进行排序。这意味着允许程序员将其当做为容器的键值,做排序,或做其他方式的比较。标准库也提供std::hashstd::thread::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();
}

虽然C++线程库为多线程和并发处理提供了较全面的工具,但在某些平台上提供额外的工具。为了方便地访问那些工具的同时,又使用标准C++线程库,在C++线程库中提供一个 native_handle() 成员函数,允许通过使用平台相关API直接操作底层实现。就其本质而言,任何使用 native_handle() 执行的操作都是完全依赖于平台的。

猜你喜欢

转载自blog.csdn.net/asmartkiller/article/details/88670687