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)
-
线程所有权的转移
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;
}
}