1.前言
最近,被期末考试AK的zzb在回顾以前的代码时,无意看到一个问题:
请问:
大佬能解释一下怎么同时运行两个c++for循环吗?
就比如说游戏里你一边出招电脑也能出招这种的
当时,zzb是用的kd来解决(详见小技巧2)
而现在,zzb要用一种新的方式:
多线程
2.正文
1.定义
摘自百度百科
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”
所以,上述问题用c++就可以很好解决
2.thread
1.定义
thread是c++多线程中的一种
在c++11时,头文件
#include<thread>
就被加入
该头文件中定义了thread类,创建一个线程即实例化一个该类的对象,实例化对象时候调用的构造函数需要传递一个参数,该参数就是函数名,thread th1(proc1);如果传递进去的函数本身需要传递参数,实例化对象时将这些参数按序写到函数名后面,thread th1(proc1,a,b);只要创建了线程对象(传递“函数名/可调用对象”作为参数的情况下),线程就开始执行(std::thread 有一个无参构造函数重载的版本,不会创建底层的线程)。
看着很绕对不对?
用样例解释一下
#include<iostream>
#include<thread>
using namespace std;
void zzb(int a){cout<<"这是线程"<<a<<"\n";}
int main()
{
cout<<"主线程:"<<endl;
//定义 线程名 函数名 参数
thread th2 (zzb, 9);
cout<<"主线程中显示子线程id为"<<th2.get_id()<<endl;//获取线程id
th2.join();//暂停主线程,运行th2
return 0;
}
运行出来的效果可能是
‘
当然,也可能是
甚至种种
(大致是因为线程运行的时间不定吧)
在网上看到一个生动的例子:
你在做某件事情,中途你让老王帮你办一个任务(你办的时候他同时办)(创建线程1),又叫老李帮你办一件任务(创建线程2),现在你的这部分工作做完了,需要用到他们的结果,只需要等待老王和老李处理完(join(),阻塞主线程),等他们把任务做完(子线程运行结束),你又可以开始你手头的工作了(主线程不再阻塞)。
而代码里面也用到了一些成员函数:
2.成员函数
函数名 | 作用 |
---|---|
get_id | 获取线程 ID |
joinable(bool) | 检查线程是否可被 join,如果线程未被join 或detach 则返回true |
join | 阻塞(暂停)当前线程直到join的线程返回 |
detach | 不阻塞当前线程,不等待该线程返回,相当于这是个守护线程。 |
swap | 交换线程 |
native_handle | e,点这里 |
hardware_concurrency | e,检测硬件并发特性(后两个zzb都不会awa) |
3.创建线程
1里面是直接创建,再来一遍加深记忆
#include<iostream>
#include<thread>
using namespace std;
void zzb(int a){cout<<"\n这是线程"<<a<<"\n";}
int main()
{
cout<<"主线程:"<<endl;
//定义 线程名 函数名 参数
thread th2 (zzb, 9);
cout<<"主线程中显示子线程id为"<<th2.get_id()<<endl;//获取线程id
th2.join();//暂停主线程,运行th2
return 0;
}
除此之外,匿名函数lambda也可以
#include<iostream>
#include<thread>
using namespace std;
int main()
{
auto zzb=[](int a){cout<<"这是线程"<<a<<"\n";};
//定义 线程名 函数名 参数
thread th2 (zzb, 9);
th2.join();
return 0;
}
class也可以
#include<iostream>
#include<thread>
using namespace std;
class node
{
public:
void zzb(int a){cout<<"这是线程"<<a<<"\n";}
}a;
int main()
{
thread th2(&node::zzb,&a,9);
th2.join();
return 0;
}
4.互斥量
1里面出现了一个神奇的bug
而对于这个bug,互斥量就可以解决
比方:
这样比喻,单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock),那么,这个许可证就是互斥量。互斥量保证了使用打印机这一过程不被打断。
互斥量在
#include<mutex>
里面
用法:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void p1(int a)
{
m.lock();//许可
cout<<"p1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
m.unlock();//归还,一定要写,不然TLE
}
void p2(int a)
{
m.lock();
cout<<"p2函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
m.unlock();
}
void p3(int a)
{
cout<<"p3函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
}
void p4(int a)
{
cout<<"p4函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
}
int main()
{
int a=0;
thread pr1(p1,a);
thread pr2(p2,a);
pr1.join();
pr2.join();
system("pause");
thread pr3(p3,a);
thread pr4(p4,a);
pr3.join();
pr4.join();
return 0;
}
效果很明显:
前面很合理,后面很核理
再详细说一下互斥量函数
5.mutex
1.lock
调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况:
1.如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
2.如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)(说白了TLE)。
2.unlock()
解锁,释放对互斥量的所有权,如果没有锁的所有权尝试解锁会导致程序异常。
3 try_lock()
尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况:
1.如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
2.如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
3.如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
4.lock_guard()
e,一个局部对象,对象内自动+-lock
例子:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void p1(int a)
{
lock_guard<mutex> g1(m);//用此语句替换了m.lock();
//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
cout<<"p1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
void p2(int a)
{
{
lock_guard<mutex> g2(m);
cout<<"p2函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
}//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
cout<<"作用域外的内容3"<<endl;
cout<<"作用域外的内容4"<<endl;
cout<<"作用域外的内容5"<<endl;
}
int main()
{
int a=0;
thread pr1(p1,a);
thread pr2(p2,a);
pr1.join();
pr2.join();
return 0;
}
当然,lock_guard传两个参数时,如果第二个为adopt_lock标识时,表示不再构造函数中不再进行互斥量锁定,因此此时需要提前手动锁定。(不然位置还是要乱)
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化m对象,不要理解为定义变量
void p1(int a)
{
m.lock();
lock_guard<mutex> g1(m,adopt_lock);//用此语句替换了m.lock();
//lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
cout<<"p1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
void p2(int a)
{
{
lock_guard<mutex> g2(m);
cout<<"p2函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
}//通过使用{}来调整作用域范围,可使得m在合适的地方被解锁
cout<<"作用域外的内容3"<<endl;
cout<<"作用域外的内容4"<<endl;
cout<<"作用域外的内容5"<<endl;
}
int main()
{
int a=0;
thread pr1(p1,a);
thread pr2(p2,a);
pr1.join();
pr2.join();
return 0;
}
当然,比起lock_guard,unique_lock更强
除了adopt_lock,它还支持try_to_lock/defer_lock
别的一样(所以谁还用lock_guard?)
try_to_lock: 尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
defer_lock: 始化了一个没有加锁的mutex;
例子:
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void p1(int a)
{
unique_lock<mutex> g1(m,defer_lock);//始化了一个**没有加锁**的mutex
cout<<"关注一下吧"<<endl;
g1.lock();//手动加锁;
//注意,不是m.lock();
//注意,不是m.lock();
//注意,不是m.lock()
cout<<"proc1函数正在改写a"<<endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+2<<endl;
g1.unlock();//临时解锁
cout<<"祝关注的同学"<<endl;
g1.lock();
cout<<"AKIOI,暴打集训队"<<endl;
}//自动解锁
void p2(int a)
{
unique_lock<mutex> g2(m,try_to_lock);//尝试加锁,但如果没有锁定成功,会立即返回,不会阻塞在那里;
cout<<"proc2函数正在改写a" << endl;
cout<<"原始a为"<<a<<endl;
cout<<"现在a为"<<a+1<<endl;
}//自动解锁
int main()
{
int a=0;
thread pr1(p1,a);
thread pr2(p2,a);
pr1.join();
pr2.join();
return 0;
}
(还是会错位,可能因为临时解锁时可能正好try_to_lock try到了)
繁琐是很繁琐,但用的时候还是真香
recursive_mutex
允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权
放互斥量时需要调用与该锁层次深度相同次数的 unlock(),可理解为 lock() 次数和 unlock() 次数相同
其余=mutex
time_mutex
比mutex多了两个函数
try_lock_for 函数接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until 函数则接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false。
#include<iostream>
#include<chrono>
#include<thread>
#include<mutex>
using namespace std;
timed_mutex m;
void p1()
{
//等lock: 每随机ms输出一次关注(还不关注)
while(!m.try_lock_for(std::chrono::milliseconds(rand()%200+1))) cout<<"关注";
//得到lock,等1s后输出点赞(还不点赞)
this_thread::sleep_for(std::chrono::milliseconds(1000));
cout<<"点赞\n";
m.unlock();
}
int main()
{
srand(time(NULL));
std::thread t[5];//线程组
for(int i=0;i<5;i++)t[i]=thread(p1);
for(auto& th:t)th.join();
return 0;
}
recursive_timed_mutex
e,你看一下recursive_mutex与mutex的区别,再类比一下(不会问老师什么是类比)