概念:软件或者硬件上实现多个线程并发执行的技术。比如我们一遍看电视,一遍吃瓜子,这就是多个线程。那相比于多进程怎么理解呢?比如电视机在放电视,洗衣机此时也在洗衣服,这就是多进程。一般来说线程是发生在进程之中的,我们把吃瓜子和看电视都理解为发生在个人活动这个进程中的。
C++11标准的实现主要使用了一个线程类,thread。至于内部的具体实现,在这里就不做介绍。
1.尽量不使用detach方法,容易引起很多问题。。。。。。
1.调用普通的函数:
void myThread(){
cout<<"我的线程开始了"<<endl;
cout<<"我的线程结束了"<<endl;
}
int mian(){
thread mythread2(myThread);
}
这是最简单的方法,对于没有值传递和交换来说,该方法较为实用
2.类成员函数的传递方法
class TA{
public:
TA(){
cout<<"TA()构造函数被执行"<<endl;
}
TA(const TA &ta){
cout<<"TA()拷贝构造函数被执行"<<endl;
}
//thread mythread(ta);
void operator ()(){
cout<<"我的线程operator ()开始了"<<endl;
cout<<"我的线程operator ()结束了"<<endl;
}
//thread mythread(bind(&TA::test2,&ta)); //band
void test2(){
cout<<"我的线程test2()开始了"<<endl;
cout<<"我的线程test2()结束了"<<endl;
}
};
int main(){
thread mythread1(ta);//直接调用重载的函数
thread mythread2(bind(&TA::test2,&ta)); //band进行参数的绑定
mythread1.join();
mythread2.join();
}
类成员函数的调用一般有两种,一种是重载()直接访问类,一种是采用Band进行参数的绑定
3.lambda表达式
int main(){
auto lambad =[]{
cout<<"我的线程test2()开始了"<<endl;
cout<<"我的线程test2()结束了"<<endl;
}
thread mythread1(lambad);
}
1.普通值得传递
对于普通值来说,最好传入const只读数据类型。
void myThread2(const int i){
cout<<"i= "<<i;
return;
}
int main(){
int i=0;
thread mythread1(myThread2,i);
}
2.引用得传递
void myThread2(const int i,vector<int> &p1){
cout<<"i= "<<i;
i=10;
p1.push_back(10);
p1.push_back(20);
p1.push_back(30);
p1.push_back(40);
return;
}
int main(){
int i=20;
vector<int> p2;
thread mythread(myThread2,i,ref(p2));
cout<<"i= "<<i<<endl;
for(auto itr=p2.begin();itr!=p2.end();itr++){
cout<<(*itr)<<endl;
}
mythread.join();
return 0;
}
对于普通值得传递和函数传递得方式是一样得,只会传入参数,不能改变主函中得参数,而对于引用来说,是可以改变主函数中得值得。记得在引用前加上ref
3.指针得传递采用move()方法
void myThread3(unique_ptr<int[]> ptr){
ptr[0]=1;
ptr[2]=2;
ptr[3]=3;
ptr[4]=4;
ptr[5]=5;
}
int main(){
unique_ptr<int[]> ptr(new int[10]);
thread mythread2(myThread3,move(ptr));
mythread2.join();
}
我们知道,move后得指针是把指针移动过去,在主函数中得指针已经不在指向之前那段内存,变成了NULL;
1.对于只读数据,线程之间得共享是安全得
const vector<int> p1={1,2,3,4,5};
void myPrint(int num){
std::cout<<"my sub thread id: "<<this_thread::get_id();
std::cout<<"|num is "<<p1[0]<<'|'<<p1[1]<<'|'<<p1[2]<<endl;
}
int main(){
vector<thread> threads;
for(int i=0;i<10;i++){
threads.push_back(thread(myPrint,i+1));
}
for(auto itr = threads.begin();itr!=threads.end();itr++){
itr->join();//等待线程结束
}
return 0;
}
2.数据有读有写得情况(加锁)
不加锁之前
class A{
public:
void getNum(){
for(int i=0;i<10000;i++){
cout<<"存入的数据为:"<<i<<endl;
data.push_back(i);
}
}
void popNum(){
for(int i=0;i<10000;i++){
if(data.empty()){
cout<<"数据不能为空"<<endl;
}
else {
cout<<"首数字为:"<<data.front();
data.pop_front();
}
}
}
private:
list<int> data;
};
int main(){
A a;
thread mythread1(&A::getNum,&a);
thread mythread2(&A::popNum,&a);
mythread1.join();
mythread2.join();
return 0;
}
程序崩溃,因为数据在发生了同时的写和读的操作,导致数据错误。
所以引入一个概念,互斥量mutex:理解为一个锁(一个class)
只有一个线程能够锁成功,锁成功后会返回一个数据
lock() and unlock()要成对使用,有lock必须有unlock 必须一一对应。
class A{
public:
void getNum(){
for(int i=0;i<10000;i++){
cout<<"存入的数据为:"<<i<<endl;
my_mutex.lock();
data.push_back(i);//保护数据段
my_mutex.unlock();
}
}
void popNum(){
for(int i=0;i<10000;i++){
my_mutex.lock();
bool flag =data.empty();
my_mutex.unlock();
if(flag){
cout<<"数据不能为空"<<endl;
}
else {
my_mutex.lock();
cout<<"首数字为:"<<data.front();
data.pop_front();
my_mutex.unlock();
}
}
}
private:
list<int> data;
mutex my_mutex;
};
void test2(){
A a;
thread mythread1(&A::getNum,&a);
thread mythread2(&A::popNum,&a);
mythread1.join();
mythread2.join();
}
加完mutex的lock和unlock后程序正常运行,不会出现错误。
为了防止忘记unlock,c++11提供了一个新的类模板lock_guard
注意该方法的作用域的范范围是{ }在一个作用域中,把需要保护的数据放到一个圆括号中,例如
{
lock_guard<mutex> myguard(my_mutex);
data.push_back(i);//保护数据段
}//括号结束,lock_guard发生析构,进行解锁
在使用多个mutex的时候可能会出现死锁的情况。什么是死锁呢?,举个例子,假设我们有两把锁,首先我们用金锁去锁住数据,然后用银锁去锁住,在第一个线程先用金锁锁住数据的时候,突然线程二进入,线程二是先锁银锁再锁金锁,这个时候由于金锁是锁住的,线程二就在等待金锁的释放,而线程一呢,发现银锁是锁住的,也在等着银锁的释放,这样一来,两个人互等。发生死锁。我们可以现实生活中理解到,假设张三在北京等着李四从深圳来再出发,而李四在深圳等着张三从北京来再出发,两人就形成了死锁。所以说,在使用两个互斥量的的时候,一定要是相同的上锁顺顺序,这样一来就不会发生死锁现象
std::lock()同时锁住多个至少两个互斥量。不会发生死锁现象。
lock_guard与lock的成对使用
void getNum(){
for(int i=0;i<10000;i++){
cout<<"存入的数据为:"<<i<<endl;
lock(my_mutex,my_mutex2);
data.push_back(i);
my_mutex.unlock();
my_mutex2.unlock();
}
}
void popNum(){
for(int i=0;i<10000;i++){
//lock_guard myguard(my_mutex);
//my_mutex.lock();
lock(my_mutex,my_mutex2);
bool flag =data.empty();
//my_mutex.unlock();
if(flag){
cout<<"数据不能为空"<<endl;
}
else {
//my_mutex.lock();
cout<<"首数字为:"<<data.front();
data.pop_front();
//my_mutex.unlock();
}
lock_guard<mutex> myguard(my_mutex,std::adopt_lock);
lock_guard<mutex> myguard2(my_mutex2,std::adopt_lock);
}
}
3 unique_lock 取代lock_guard
1.std::adopt_lock 表示线程已经提前lock,不需要再构造函数中在此lock
2.std::try_to_lock尝试用lock去锁定mutex,哪怕没锁上也会立即返回。前提是不能用lock
class A{
public:
void getNum(){
for(int i=0;i<10000;i++){
cout<<"存入的数据为:"<<i<<endl;
unique_lock<mutex> mylock(my_mutex,try_to_lock);
if(mylock.owns_lock()){
data.push_back(i);
}
else {
cout<<"拿不到锁。。。。。。"<<endl;
}
}
}
void popNum(){
for(int i=0;i<10000;i++){
//lock_guard myguard(my_mutex);
//my_mutex.lock();
lock(my_mutex,my_mutex2);
bool flag =data.empty();
Sleep(10);
//my_mutex.unlock();
if(flag){
cout<<"数据不能为空"<<endl;
}
else {
//my_mutex.lock();
cout<<"首数字为:"<<data.front();
data.pop_front();
//my_mutex.unlock();
}
lock_guard<mutex> myguard(my_mutex,std::adopt_lock);
lock_guard<mutex> myguard2(my_mutex2,std::adopt_lock);
}
}
private:
list<int> data;
mutex my_mutex;
mutex my_mutex2;
};
void test2(){
A a;
thread mythread1(&A::getNum,&a);
thread mythread2(&A::popNum,&a);
mythread1.join();
mythread2.join();
}
3.defer_lock 并没有加锁,初始化一个mutex类。
uniquck_lock 有自带的lock和unlock方法,可以随时解锁,上锁。解锁后处理非共享数据,枷锁后处理共享数据
本章节主要介绍了一些多线程的基本用法,能够满足基本的使用。能够满足简单的实现多线程代码。
下篇将继续深入探讨多线程的内容。