C++11线程学习(1)—— 创建+分离+分配+线程ID

1, 创建一个线程

1.1 可以通过函数指针创建
#include <thread>
#include <iostream>


static void worker1(){
	for (int i = 0; i < 10; ++i) {
		std::cout << "worker " << i << std::endl;
	}
}


int main() {

	std::thread t1 = std::thread(worker1);

	if (t1.joinable()){
		std::cout << "wait worker1" << std::endl;
		t1.join();
	}


	std::cout << "===end===" << std::endl;
	return 0;
}

此处使用了join函数来测试线程的创建,如果不适用main函数的线程会限于worker1结束。
打印如下(多次)
在这里插入图片描述
在这里插入图片描述
此处可以发现worker1线程和主线程是同时执行的,也就是说worker1和主线程同时在向打印输出流中输出内容,而导致不一致。如果想要一致则必须要使得worker1和主线程完成同步(之后讨论)。

1.2 可以通过对象来创建

通过类的实例化对象来实现线程,其实是对()操作符的重载操作来实现线程的启动,这里我们需要注意一点,该对象是通过复制到新的线程中来执行的而不是原来的对象被执行,这一点我们可以通过复制构造函数来检测。

class worker2 {
public:
	worker2(){
		count++;
	}
	
	worker2(worker2 const& w2) {
		count++;
	}

	void operator() () {
		std::cout << "worker2 " << "[" << count << "]"<< std::endl;
	}

	static int count;
};

int worker2::count = 0;


		/* worker2 test */
		
		worker2 w2 = worker2(); //...................1

		w2();

		std::thread t2 = std::thread( w2 ); //................2


		if (t2.joinable()) {
			//std::cout << "wait worker2" << std::endl;
			t2.join();
		}

结果:
在这里插入图片描述
这里我们设置了一个全局变量来辅助我们检测,首先在1处worker2的构造函数被调用count++,在2处worker2的复制构造函数被调用count++。这意味着如果我们以对象的形式创建线程,一定要注意它仅仅是一个copy在另一个栈空间中重新执行,如果有指针务必检查使用以免出现悬垂指针。

1.3 可以通过Lambda来创建

其实就是匿名函数,相当灵活。

		std::thread t3 = std::thread([](int x) {
			std::cout << "worker3 working..." << "[" << x << "]" << std::endl;
		},33);
		
		if (t3.joinable()) {
			//std::cout << "wait worker2" << std::endl;
			t3.join();
		}

(这边需要注意的是,如果我们不等待新开线程的结束,有可能主线程先于新开线程结束,而导致程序收到abort信号)

2.分离或者等待一个线程

	2.1 先说一下啊接口把 ,join 和 detach
std::thread t3 = std::thread([](int x) {
			//std::cout << "worker3 working..." << "[" << x << "]" << std::endl;
		},33);
		
		t3.detach();

		if (t3.joinable()) {
			std::cout << "wait worker2" << std::endl;
			t3.join();
		}else {
			std::cout << "thread is not joinable" << std::endl;
		}
一个是等待线程,一个是分离线程,很多情况下我们都是要分离线程,如果还要可以去等待,那么干嘛还要另起一个线
程浪费时间呢?

此外,在学习的时候还学习到了一种线程的RAII(Resource Acquisition Is Initialization)机制,简单解释一下就是资源
的创建就是初始化,对于资源的理解,我理解的比较粗糙,凡是需要消耗内存的都是在使用资源。那么线程也是可以看
作是我们的一个资源,**所以一个类如果在实现过程中需要使用线程,那么在这个类生命周期结束时,也是需要释放
的,故应该在析构函数中等待线程执行完毕。

此外,如果某个线程希望等待另一个线程,那么还需要考虑是否会发生异常,故此时放在finally里面就好了。
class thread_guard {
public:
	std::thread t;

	explicit thread_guard() {
		
	}

	~thread_guard() {
		if (t.joinable()) {
			t.join();
			std::cout << "can join" << std::endl;
		}else {
			std::cout << "can't join" << std::endl;
		}
	}

	void work() {
		t = std::thread([]() {
			int i = 0;
			for (; i < 10000; ++i) {
				
			}
			std::cout << "thread_guard work done" << std::endl;
		});
	}
};

//test codes
		thread_guard g;
		g.work();
2.2 参数的传入
		通过thread接口构建的线程,可以在函数指针后加入参数,来实现向线程执行处的参数传入。需要注意的是,这里
	传入的都是值的copy,包括引用也是copy。那么对于这样的情况只能够通过在堆上分配内存,并传入地址,这会增加
	内存泄漏的风险。还有一种情况是通过 `std::ret(XXX)` 来把引用传入。
		有的变量在一个时刻只能有一个用例,这时需要把控制权进行转移。(eg: std:unique_ptr)

  1. 线程所有权的转移

    3.1同一时间只能有一个对象拥有所有权
    	 类似于unique_ptr,我们必须明确线程的所有权,线程的所有权在任意时间只能有一个。换句话
    说说,我如有拥有线程的所有全权,就可以调用join,然而没有了所有权就不可以再调用。看代码
    
class scoped_thread {
	std::thread m_t;
public:
	//!!! 注意这里传入是线程的对象,而不是引用
	explicit scoped_thread(std::thread t_):m_t(std::move(t_)) {
		if (!m_t.joinable()) {
			throw std::logic_error("No thread");
		}
	}

	~scoped_thread(){
		if (m_t.joinable()) {
			m_t.join();
		}
	}
	/* 删除 拷贝构造函数 和 赋值的重载*/
	scoped_thread(scoped_thread const&) = delete;
	scoped_thread& operator=(scoped_thread const&) = delete;
};

//=======================Test====================
		try
		{
			std::thread t5 = std::thread([]() {
				int i = 0;
				for (; i < 10000; ++i) {

				}
				std::cout << "case 5 done [" << i << "]" << std::endl;
			});

			scoped_thread st(std::move(t5));	//..................	1
			//scoped_thread st2(t5);		//.................... 2

		}catch (...){
			std::cout << "catch error" << std::endl;
		}

因为这里我们的对象传入的是线程的对象,所以不能传入一个线程的对象,正如测试代码的1,2所示。
如果传入t5这个对象,编译器就会报错。

严重性	代码	说明	项目	文件	行	禁止显示状态
错误	C2280	“std::thread::thread(const std::thread &): 尝试引用已删除的函数	thread_demo	e:\workspace\c++\thread_demo\thread_demo\ch2.cpp	147	

因为同时我们可以用两个对象来控制这个线程,所以再底层,已经禁止了这样的copy。

4.选择线程的数量
	我们知道一个处理器,可能会有多核,那么我们不应该只使用一核也不应该超额订阅(运行远多于硬件
	支持数目的),所以思路就是根据合理的线程数量来分配任务,std::thread里面提供了接口
	`std::thread::hardware_concurrency()` 用来获取能够真正运行的线程数量,这个我专门去官网查了一
	下,有时候结果可能不准,不准确时会返回0。下面这个例子我觉得质量还可以,先解释一下几个库函
	数思`std::accumulate(begin, end, result)`从开始累加到最后,再result的基础上累加,有其他重载版
	本。 希望用最简单的思路解释一下,1,根据任务数和线程的数量来计算合适的线程数(这一点我有点
	没明白max_thread 的计算) 2,开一个vector来保持线程所有权,和一个vector来计算结果  3,把没有
	分配的任务走完 4 继续求和

这一类似于归并排序,分配最小的任务,然后累计求和,下一篇文章讲完同步的时候,我会尝试用线程归并求和(给某梨写个demo)。

template<typename Iterator, typename T>
struct accumulate_block
{
	void operator()(Iterator begin, Iterator end, T& result) {
		result = std::accumulate(begin, end, result);
	}
};

template<typename Iterator, typename T>
T parallel_accumulate(Iterator begin, Iterator end, T init) {
	unsigned long const length = std::distance(begin, end); //计算迭代器的从开始到结束的长度

	if (length == 0)
		return init;

	unsigned long const min_per_thread = 25;
	unsigned long const max_thread = (length + min_per_thread - 1) / min_per_thread;

	unsigned long const hardware_threads = std::thread::hardware_concurrency();
	std::cout << "max_thread: " << max_thread << std::endl;
	std::cout << "hardware_threads: " << hardware_threads << std::endl;

	unsigned long const nums_threads = std::min(hardware_threads != 0 ? hardware_threads : 2, max_thread); //此处计算出线程的合理大小
	unsigned long const block_size = length / nums_threads;

	std::vector<T> results(nums_threads); //用于存储计算结果
	std::vector<std::thread> threads(nums_threads - 1); //除去当前线程

	Iterator block_start = begin; //用于分块遍历整个迭代器来分配给多线程任务

	for (auto i = 0; i < (nums_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, end, results[nums_threads - 1]); //用来计算不能整除的部分

	std::cout << "waiting thread caculate...." << std::endl;

	for (auto it = threads.begin(); it != threads.end(); ++it) {
		(*it).join();
	}
	std::cout << "thread caculate done." << std::endl;
	return std::accumulate(results.begin(), results.end(), init);
}

============TEST========================
std::vector<int> nums = {1,2,3,4,5,6,7,8,9,10,101};

		std::cout << "start case 6..." << std::endl << std::endl;
		int ret = parallel_accumulate<std::vector<int>::iterator, int>(nums.begin(), nums.end(), 0);
		std::cout << "case 6 done, ret [" << ret << "]"<< std::endl;

结果:
在这里插入图片描述
5.获取线程id,线程的id可以理解为线程的mac地址,唯一的可以用来做排序,也可以用来做hash的键值,用来比较都是ok的。下面时一个用来做键值的例子。

/*
* 获取线程的ID
*/
#include <unordered_map>
#include <string>
void get_thread_id() {
	std::unordered_map<std::thread::id, std::string> thread_map;

	std::vector<std::thread> threads(5);

	for (auto i = 0; i < 5; ++i) {
		threads[i] = std::thread([i]() {
			std::cout << i << std::endl;
		});
		thread_map.insert(std::pair<std::thread::id, std::string>(threads[i].get_id(), "thread" + std::to_string(i)));
	}

	for (auto i = 0; i < 5; ++i) {
		threads[i].join();
	}

	for (auto it = thread_map.begin(); it != thread_map.end(); ++it) {
		std::cout << it->first << " | " << it->second << std::endl;
	}
}

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值