C++多线程知识点梳理
0.前言
本篇主要围绕C++线程知识展开,本篇并不会一次性写完,而是会不断填充,以便后续回看。
1.线程基础概念
- 线程与进程的区别:线程是进程的执行单元,共享进程资源(内存、文件描述符等),切换开销更小。
- 并发与并行的区别:并发是任务交替执行,并行是任务同时执行(依赖多核CPU)。
- C++标准库支持:
<thread>
头文件提供线程管理接口。
2.C++线程常用接口
2.1创建线程
通过std::thread
构造函数,传入可调用对象(函数、Lambda、成员函数等)。从下面可以看到常用的线程创建方式。
#include<iostream>
#include<thread>
using namespace std;void test(){cout<<"test"<<endl;
}void test1(int i){cout<<"test(int i)"<<i<<endl;
}void test2(int& i){i++;cout<<"test(int i)"<<i<<endl;
}class ThreadDemo{public:void test(int i){cout<<"ThreadDemo::test(int i)"<<i<<endl;}
};int main(void){int i = 1;// 情形1:全局函数创建线程thread th1(test);thread th2(test1,i);// 情形2:成员函数创建线程ThreadDemo td;thread th3(&ThreadDemo::test,&td,i);// 引用传参thread th4(test2,std::ref(i));this_thread::sleep_for(1000ms);// 情形3:lambda创建线程thread th5([&i](){i = i+1;cout<<"lambda"<<i<<endl;});th1.join();th2.join();th3.join();th4.join();th5.join();cout<<i<<endl;return 0;
}
这里需要着重注意的是,如果采用引用(或指针)传参,一定要保证线程在执行完毕前,其参数不能被销毁。
2.2线程回收
常见的线程回收包括:join()
等待线程结束,detach()
分离线程(后台运行)。
2.3线程参数
前文提到过,如果采用拷贝给线程传递参数安全性较高,但是如果传递引用参数的话,必须注意参数在线程使用过程中不能被释放。
比如有如下代码:
struct threaddemo
{int& i;threaddemo(int& i_) : i(i_) {cout<<&i<<endl;}void test(int i){cout<<i<<endl;}void operator() (){for (unsigned j=0 ; j<10 ; ++j){test(i); // 访问局部变量引用}}
};int main(void){{int l_val=0;threaddemo tm(l_val);thread my_thread(tm);my_thread.detach(); // 2. 不等待线程结束}return 0;
}
如果实例化上述类中的对象并创建线程(main中),则会间接引用到局部变量,会出现安全问题。
2.4线程管理
每次创建一个线程都需要找个时机调用join(),这确实有点反人类了。常见的方法是采用RAII的方法。完整的代码大概是这样的:
#include<thread>
#include<iostream>
using namespace std;struct threaddemo
{int& i;threaddemo(int& i_) : i(i_) {cout<<&i<<endl;}void test(int i){cout<<i<<endl;}void operator() (){for (unsigned j=0 ; j<10 ; ++j){test(i); // 1. 潜在访问隐患:悬空引用}}
};class RAIIthread{
public:thread& th;RAIIthread(thread& th_) : th(th_) {}~RAIIthread(){if(th.joinable())cout<<"exec"<<endl;th.join();}
};int main(void){{int l_val=0;cout<<&l_val<<endl;threaddemo tm(l_val);thread my_thread(tm);RAIIthread raii_thread(my_thread);// my_thread.join(); // 2. 不等待线程结束}return 0;
}
这样每次线程执行完毕后,就可以自动调用join了。
3.共享数据保护
当多个线程同时操作共享数据(或代码段)时,可能存在竞争问题。为此,C++采用了锁机制解决这个问题。
3.1mutex基本使用
这里直接给个简单的例子:在多线程场景下向一个数组中添加元素。代码实现如下:
#include<iostream>
#include<vector>
#include<thread>
#include<mutex>using namespace std;// 向容器添加数据的任务
mutex m;
void addTask(vector<int>& vec,int i){m.lock();for(int j = 0;j<i;j++){vec.push_back(j);}m.unlock();
}// 打印容器元素
void printVec(const vector<int>& vec){for(auto i:vec){cout<<i<<" ";}cout<<endl;
}
//
int main(void){vector<int> data;vector<thread> ths;for(int i = 0;i<5;i++){ths.push_back(thread(addTask,std::ref(data),1000));}// 回收线程for(auto &th:ths){th.join();}printVec(data);return 0;
}
在主函数中创建了五个线程,每个线程都会向vector添加1000个元素。因为加了mutex的原因,程序输出的vector大小是5000个。但是不加锁则一定几率出现小于5000个元素,甚至会报错。
3.2RAII+mutex
从3.1程序中可以看到,锁的释放需要特定的时机。C++标准库提供了std::lock_guard模板类,它能在创建时自动加锁,并在析构时自动解锁,即RAII。
那任务函数可以简化为:
void addTask(vector<int>& vec,int i){lock_guard<mutex> lg(m);for(int j = 0;j<i;j++){vec.push_back(j);}
}
补充
后续内容慢慢补充!