【线程管理】——CH2

1.线程管理的基础

1.1启动线程

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

关于一个thread对象是否是joinable:

如果一个线程正在执行,那么它是joinable的

下列任一情况,都是非joinable:

A:默认构造器构造的。

B:通过移动构造获得的。

C:调用了join或者detach方法的。

版权声明:以上图片为CSDN博主「小罗tongxue」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44843859/article/details/112170599

提供的函数对象会复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。

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

std::thread my_thread(background_task()); //编译器将其看为一个函数
class background_task
{
public:
 void operator()() const //伪函数
{
  do_something();
  do_something_else();
}
};

可以使用: 

1.命名对象的方式:

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

2.多组括号

std::thread my_thread( (background_task()) )

3.新统一的初始化语法

std::thread my_thread{background_task()}

4.使用lambda表达式:允许使用一个可以捕获局部变量的局部函数

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

启动了线程,必须明确是等待线程结束(加入式),还是让其自主运行(分离式)。如果std::thread对象销毁前还没做出决定,程序就会终止(std::thread析构函数会调用std::terminate())。因此,即便有异常存在,也需要确保线程能够正确加入或分离。

如果不等待线程,就必须保证线程结束之前,可访问数据的有效性。如果线程还没结束,主函数已经退出,这时线程函数还持有函数局部变量的指针或引用,就会引发错误。

处理这种情况的常规方法:

a.使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中(共享数据第三章介绍,后边在补概念);

b.此外,可以通过join()函数确保线程在函数完成前结束。

1.2等待线程完成

需要等待线程完成,相关std::thread实例需要使用join()。

如果需要对等待时间中的线程有更灵活的控制,比如只等待一段时间,需要使用其他机制来完成,比如条件变量和期待。

只能对一个线程使用一次join()的行为,因为该行为清理了线程相关的存储部分,std::thread对象与已经完成的线程没有任何关联了。

1.3特殊情况下等待

为了避免在join()还未调用前,程序就因异常而终止这样的情况,我们需要在异常处理过程中调用join(),从而避免生命周期的问题。

方法一:使用try/catch块

struct func;
	void f()
	{
		int some_local_state = 0;
		func my_func(some_local_state); //结构体里有伪函数,传递的参数用于构造这个结构体
		std::thread t(my_func);         //线程开启
		try 
		{
			do_some_thing_in_current_thread();   //正常运行
		}
		catch (...)
		{
			t.join();                  //发生异常,先加入,在抛出异常
			throw;
		}
		t.join();                  //正常退出线程,会在这里加入
	}

方法二:使用RAII(资源获取即初始化方式)等待线程完成

//提供一个类,在析构函数中使用join()
	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();
	} //当程序结束运行时,将会倒序销毁局部对象,g是第一个销毁的,在析构函数中,判断t是否已经加入

1.4后台运行线程

方法:使用detach()

解释:使用detach()会让程序在后台运行,主线程和线程不能直接进行交互,并且std::thread()对象不能再引用线程,但C++运行库保证,线程退出后,相关资源能够正确的回收。

意义:分离线程也被称为守护线程,因为没有任何显式用户接口,线程的生命周期可能会很长,例如后台监视文件系统、对缓存进行清理等应用。

条件:要从std::thread()对象中分离线程,必须这个线程是joinable()才可以。(先判断,再分离)

示例:文字处理应用同时编辑多个文档

//让每个文档处理窗口都有自己的线程;每个线程运行相同的代码,并隔离不同窗口的数据
	///因为是对独立的文档进行操作,没必要等其他线程完成
	//让文档处理窗口运行在分离的线程上
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); //传递参数
				t.detach();
			}
			else
			{
				process_user_input(cmd);
			}
		}
	}

2.向线程中传递参数

注意:std::thread对象默认对所有参数都进行复制拷贝,即便是参数引用的形式。

2.1正常情况

void edit_document(std::string const& filename);
std::thread t(edit_document, new_name); //传递参数

向std::thread对象构造函数直接传递参数。 

2.2涉及到参数的隐式转换

如果不涉及指针,在线程的上下文中会自动完成类型的隐式转换,但如果传入的是一个指针,由于对默认参数的拷贝只会拷贝数据,并不会拷贝该数据是什么类型,就会导致std::thread的构造函数不知道该将其转换为什么类型,这就是悬垂指针,为了避免该情况,应该在构造函数之前就将指针主动给转换为正确类型。

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);      //不对
		std::thread t(f,3,std::string(buffer))  //正确
	}

2.3传递引用参数

使用std::ref将参数转换为引用形式,

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

2.4传递动态对象

std::thread对象提供的参数可以移动,不能拷贝(移动类似剪切),类似于智能指针,只能指向一个对象,当这个指针被销毁,指向的对象也会被删除。使用移动转移原对象后,就会留下一个空指针。

(仅当需要的参数 原对象是个动态且是一个命名变量才需要这样)移动操作可以将对象转换为可接受的类型。当原对象是一个临时变量,自动进行移动操作,当原对象是一个命名变量,转移的时候需要使用std::move()进行显式移动。

示例: 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_data, std::move(p));

big_object对象所有权收益按转移到新建立线程的内部存储,之后传递给process_big_object函数。 

std::thread不像std::unique_ptr那样能占有一个对象的所有权,但能占有其他资源,执行线程的所有权可以在多个std::thread对象中转移,(std::thread实例可移动且不可复制性),不可复制性保证了在同一时间,一个std::thread实例只能关联一个执行线程。

3.转移线程所有权

原因:创建一个线程,并在函数中转移所有权,都必须等线程结束,想在线程不结束的时候直接对线程所有权进行转移。

示例一:

void some_function();
	void some_other_function();
	std::thread t1(some_function);     
	std::thread t2 = std::move(t1);         //t1的所有权转移给t2,与t1无关
	t1 = std::thread(some_other_function);   //一个临时std::thread开启了新线程,转移给t1,
                                              //为什么不使用std::move,因为是临时变量,移动
                                              //操作隐式调用
	std::thread t3;                          //默认方式构造t3
	t3 = std::move(t2);                      //t2线程所有权转移给t3
	t1 = std::move(t3);                      //t1有线程,转移失败,终止程序,不抛出异常
                                           //保证与std::thread的析构函数的行为一致

在线程对象被析构前,显式的等待线程完成,或者分离它,赋值同样需要满足这些条件。

terminate()函数调用exit()结束当前程序时,会调用析构函数;

terminate()函数调用abort()结束当前程序时,不会调用析构函数,防止在析构函数中抛出异常。

C++默认调用abort()函数,并且不要在析构函数中抛出异常,因为析构函数是用来释放资源的,如果抛出异常,资源无法释放,会导致全局的结束函数terminate()函数被反复调用。

Terminate()是整个程序释放系统资源最后的机会。

std::thread支持移动,表明线程所有权可以在函数外进行转移:

示例二:函数返回一个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));  //t所对应的线程传递给函数f中
	}
	

示例四:

//为了保证线程程序退出前完成,定义scope_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();
	}

 新线程直接传递到scoped_thread中,而非创建一个独立变量。thread_guard类在析构中检查线程是否可以加入,这里把检查放在了构造函数中,并且当线程不可加入时,抛出异常。

4.运行时线程数量的确定

std::thread::hardware_concurrency()函数能返回能在一个程序中并发的线程数量。例如,多核系统,返回值可以是CPU核芯的数量。返回值仅是一个提示,当系统信息无法获取,函数也会返回0。

原生并行版std::accumulate()函数的实现:

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 paraller_accumulate(Iterator first, Iterator last, T init)
	{
		unsigned long const length = std::distance(first, last); //判断长度

		if (!length)
			return init;              //长度为0,返回默认值
		unsigned long const min_per_thread = 25; //每分钟线程处理的数据量
		unsigned long const max_threads = (length + min_per_thread - 1) / min_per_thread;       //计算需要使用的线程数量(数据量来计算)
		unsigned long const hard_threads = std::thread::hardware_concurrency();

		unsigned long const num_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads); //在max_threads和hard_threads(2)选取较小那个

		unsigned long const block_size = length / num_threads;               //真正数量
		
		std::vector<T> results(num_therads);                 //存放中间结果
		std::vector<std::therad> 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);   //前进n
			threads[i] = std::thread(accumulate_block<Iterator, T>(), block_start, block_end, std::ref(result[i]));
			block_start = block_end;
		}
		accumulate_block<Iterator, T>() (block_start, last, results[nums_therads - 1]);  //处理最终块的结果
		                                                              //最终块的数据可能不满,单独处理
		
		//当累加最终块的结果后,让所有线程join()
		std::for_each(threads.begin(), threads.end(), std::mem_fn(&std::thread::join));

		return std::accumulate(results.begin(), results.end(), init);  //结果累加
	}
	

注意:因为不能直接从一个线程中返回一个值,所以需要传递results容器引用到线程中去;还可以使用地址来获取线程的结果(CH4介绍)。

例子中,T类型的加法运算不满足结合律(float和double数据相加,可能会有截断操作)可能会导致parallel_accumulate得到结果与std::accumulate结果不同。

迭代器必须是前向迭代器,std::accumulate只要是迭代器就可以工作。

创造出results容器,必须保证T有默认构造函数。

对于线程运行时,必须所有的信息都要传递到线程中去,包括存储计算结果的位置。但并非总是如此,如何是一个识别线程的任务,可以传递一个标识数。

5.标识线程

线程标识类型为 std::thread::id,可以通过两种方式检索。

方法一

调用std::thread对象的成员函数get_id()来直接获取,如果对象与没有与线程相关,函数将返回std::thread::type默认构造值。

方法二

在当前线程中调用std::this_thread::get_id()。

作用:

std::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_commom_work();
	}

可以将当前线程的std::thread::id存储到一个数据结构中,之后在这个结构体中对当前线程ID与存储的线程ID做对比,来决定操作。

此外,线程和本地存储不适配,可以将线程ID在容器中作为键值,作为将线程存储到本地的代替方案。

std::thread::id可以作为线程的通用标识符,当标识符只与语义相关(类似数组索引),就可以这样使用,也可以将id输出来记录std::thread::id对象的值。

std::cout<<std::this_thread::get_id();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值