c++多线程并发思想:将任务的不同功能分给多个函数实现,并由每个线程执行一个函数,所以一个任务就可以由不同的线程同时执行。(并发的本质仍然是串行执行各个任务,只是宏观看起来是并行的状态,核心是各个线程采用时间片轮询的方式抢占cpu执行权。)
何时不使用线程并发:不使用并发的唯一原因就是收益(性能的增幅)比不上成本(代码开发的脑力成本、时间成本,代码维护相关的额外成本)。运行越多的线程,操作系统需要为每个线程分配独立的栈空间,需要越多的上下文切换,这会消耗很多操作系统资源,如果在线程上的任务完成得很快,那么实际执行任务的时间要比启动线程的时间小很多,所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能,此时收益就比不上成本。
Warning:在使用vs编写thread操作时,system("pause")本质上应该就是阻塞了main函数这个线程,阻止了main这个线程的消亡,然后因为主线程没有销毁,因此会让其子线程也全部执行完毕,因此在写线程操作时不应添加system该函数
0.创建线程:
首先要引入头文件#include<thread>,管理线程的函数和类在该头文件中声明,其中包括std::thread类。
语句"std::thread th1(proc1);"创建了一个名为th1的线程,并且线程th1开始执行。
实例化std::thread类对象时,至少需要传递函数名作为参数。如果函数为有参函数,如"void proc2(int a,int b)",那么实例化std::thread类对象时,则需要传递更多参数,参数顺序依次为函数名、该函数的第一个参数、该函数的第二个参数,如"std::thread th2(proc2,a,b);"。
注意:如果在thread th1(fun1,x)其中的x想要使用引用的值,此时必须加上ref(x),即修改为:thread th1(fun1,ref(x)).
栗子🌰_0:
void fun1(int num)
{
for (int i = 0; i < num; i++)
{
cout << "this is Fun_1" << endl;
}
}
int main()
{
thread th1(fun1,3); //第一个参数为函数名,剩下的参数是函数依次需要传的参数
system("pause>nul");
return 0;
}
1.线程阻塞方法:
join()与detach()都是std::thread类的成员函数,两者的区别是是否等待子线程执行结束。
join()等待调用线程运行结束后当前线程再继续运行,例如,主函数中有一条语句th1.join(),那么执行到这里,主函数阻塞,直到线程th1运行结束,主函数再继续运行。
整个过程就相当于:你在处理某件事情(你是主线程),中途你让老王帮你办一个任务(与你同时执行)(创建线程1,该线程取名老王),又叫老李帮你办一件任务(创建线程2,该线程取名老李),现在你的一部分工作做完了,剩下的工作得用到他们的处理结果,那就调用"老王.join()"与"老李.join()",至此你就需要等待(主线程阻塞),等他们把任务做完(子线程运行结束),你就可以继续你手头的工作了(主线程不再阻塞)。
(PS:一提到join,你脑海中就想起两个字,"等待",而不是"加入",这样就很容易理解join的功能。)
调用join()会清理线程相关的存储部分,这代表了join()只能调用一次。使用joinable()来判断join()可否调用。同样,detach()也只能调用一次,一旦detach()后就无法join()了,有趣的是,detach()可否调用也是使用joinable()来判断。
总结:join()函数的作用是让主线程的等待该子线程完成,然后主线程再继续执行。这种情况下,子线程可以安全的访问主线程中的资源。子线程结束后由主线程负责回收子线程资源。一个子线程只能调用join()和detach()中的一个,且只允许调用一次。可以调用joinable()来判断是否可以成功调用join()或detach()。
(可用joinable判断join和detach的使用次数是否超过一次)
在一个线程中,开了另一个线程去干另一件事,使用join函数后,原始线程会等待新线程执行结束之后,再去销毁线程对象。
这样有什么好处?
---->因为它要等到新线程执行完,再销毁,线程对象,这样如果新线程使用了共享变量,等到新线程执行完再销毁这个线程对象,不会产生异常。如果不使用join,使用detch,那么新线程就会与原线程分离,如果原线程先执行完毕,销毁线程对象及局部变量,并且新线程有共享变量或引用之类,这样新线程可能使用的变量,就变成未定义,产生异常或不可预测的错误。
(PS:以detach的方式执行线程时,要将线程访问的局部数据复制到线程的空间(使用值传递),一定要确保线程没有使用局部变量的引用或者指针,除非你能肯定该线程会在局部作用域结束前执行结束。当然,使用join方式的话就不会出现这种问题,它会在作用域结束前完成退出。)
所以,当你确定程序没有使用共享变量或引用之类的话,可以使用detch函数,分离线程。
但是使用join可能会造成性能损失,因为原始线程要等待新线程的完成,所以有些情况(前提是你知道这种情况,如上)使用detch会更好。
栗子🌰_1:
void fun1(int num)
{
for (int i = 0; i < num; i++)
{
cout << "this is Fun_1:" <<i << endl;
}
}
int main()
{
cout << "Main thread is processing!" << endl;
thread th1(fun1, 100); //第一个参数为函数名,剩下的参数是函数依次需要传的参数
th1.detach();
//system("pause>nul");
return 0;
}
//使用detach()会不阻塞主线程,因此主线程先执行完毕后将th1子线程销毁,所以th1子线程并没有执行完毕。
栗子🌰_2:
void fun1(int num)
{
for (int i = 0; i < num; i++)
{
cout << "this is Fun_1:" <<i << endl;
}
}
int main()
{
cout << "Main thread is processing!" << endl;
thread th1(fun1, 100); //第一个参数为函数名,剩下的参数是函数依次需要传的参数
th1.join();
//system("pause>nul");
return 0;
}
//使用join()将阻塞主线程,必须将th1子线程全部执行完毕后才会释放再响应主线程。
栗子🌰_3: 最重要的对比实验
void fun1(int num)
{
for (int i = 0; i < num; i++)
{
cout << "this is Fun_1:" <<i << endl;
}
}
int main()
{
cout << "Main thread is processing!" << endl;
thread th1(fun1, 100); //第一个参数为函数名,剩下的参数是函数依次需要传的参数
Sleep(1);
th1.detach();
//system("pause>nul");
return 0;
}
//增加主线程1秒的睡眠时间,可发现当子线程在这一秒空档期内执行到第23项循环后,主线程才执行完毕,然后导致th1被跟随主线程销毁,导致子线程th1在第23项时停止
2.lock()与unlock(),加锁与释放锁:
什么是互斥量(锁)?
这样比喻:单位上有一台打印机(共享数据a),你要用打印机(线程1要操作数据a),同事老王也要用打印机(线程2也要操作数据a),但是打印机同一时间只能给一个人用,此时,规定不管是谁,在用打印机之前都要向领导申请许可证(lock),用完后再向领导归还许可证(unlock),许可证总共只有一个,没有许可证的人就等着在用打印机的同事用完后才能申请许可证(阻塞,线程1lock互斥量后其他线程就无法lock,只能等线程1unlock后,其他线程才能lock)。那么,打印机就是共享数据,访问打印机的这段代码就是临界区,这个必须互斥使用的许可证就是互斥量(锁)。
互斥量是为了解决数据共享过程中可能存在的访问冲突的问题。这里的互斥量保证了使用打印机这一过程不被打断。
栗子🌰_1:
#include <thread>
#include <mutex>
int num = 0;
mutex m;
void fun1()
{
m.lock();
cout << "TH1原num:" << num << endl;
num = num + 10;
cout << "TH1现num:" << num << endl;
m.unlock();
}
void fun2()
{
m.lock();
cout << "TH2原num:" << num << endl;
num = num + 10;
cout << "TH2现num:" << num << endl;
m.unlock();
}
int main()
{
cout << "Main thread is processing!" << endl;
thread th1(fun1); //第一个参数为函数名,剩下的参数是函数依次需要传的参数
thread th2(fun2);
th1.join();
th2.join();
cout << "Main thread's num:" << num << endl;
//system("pause>nul");
return 0;
}
需要在进入临界区之前对互斥量加锁lock,退出临界区时对互斥量解锁unlock;当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。
3.lock_guard()监管互斥量:
td::lock_guard()是什么呢?它就像一个保姆,职责就是帮你管理互斥量,就好像小孩要玩玩具时候,保姆就帮忙把玩具找出来,孩子不玩了,保姆就把玩具收纳好。
其原理是:声明一个局部的std::lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。最终的结果就是:创建即加锁,不能中途解锁,作用域结束自动解锁。从而使用std::lock_guard()就可以替代lock()与unlock()。
通过设定作用域,使得std::lock_guard在合适的地方被析构(在互斥量锁定到互斥量解锁之间的代码叫做临界区(需要互斥访问共享资源的那段代码称为临界区),临界区范围应该尽可能的小,即lock互斥量后应该尽早unlock),通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁:
栗子🌰_1:
mutex m;
void proc1(int a)
{
lock_guard<mutex> g1(m);//用此语句替换了m.lock();lock_guard传入一个参数时,该参数为互斥量,此时调用了lock_guard的构造函数,申请锁定m
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
}//此时不需要写m.unlock(),g1出了作用域被释放,自动调用析构函数,于是m被解锁
4.unique_lock ()监管互斥量:
unique_lock是一个通用的互斥量锁定包装器,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。
简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。
特点如下:
- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
- 可以随时加锁解锁
- 作用域规则同 lock_grard,析构时自动释放锁
- 不可复制,可移动
参考文献:https://www.jianshu.com/p/34d219380d90
5.异步线程:
需要#include<future>
async与future:
std::async是一个函数模板,用来启动一个异步任务,它返回一个std::future类模板对象,future对象起到了占位的作用(记住这点就可以了),占位是什么意思?就是说该变量现在无值,但将来会有值(好比你挤公交瞧见空了个座位,刚准备坐下去就被旁边的小伙给拦住了:“这个座位有人了”,你反驳道:”这不是空着吗?“,小伙:”等会人就来了“),刚实例化的future是没有储存值的,但在调用std::future对象的get()成员函数时,主线程会被阻塞直到异步线程执行结束,并把返回结果传递给std::future,即通过FutureObject.get()获取函数返回值。
相当于你去办政府办业务(主线程),把资料交给了前台,前台安排了人员去给你办理(std::async创建子线程),前台给了你一个单据(std::future对象),说你的业务正在给你办(子线程正在运行),等段时间你再过来凭这个单据取结果。过了段时间,你去前台取结果(调用get()),但是结果还没出来(子线程还没return),你就在前台等着(阻塞),直到你拿到结果(子线程return),你才离开(不再阻塞)。
栗子🌰_1:
#include <thread>
#include <mutex>
#include <future>
mutex m;
void fun1(int &num)
{
//m.lock();
cout << "TH1原num:" << num << endl;
num = num + 10;
cout << "TH1现num:" << num << endl;
//m.unlock();
}
void fun2(int &num)
{
//m.lock();
cout << "TH2原num:" << num << endl;
num = num + 10;
cout << "TH2现num:" << num << endl;
//m.unlock();
}
int main()
{
int num = 0;
cout << "Main thread is processing!" << endl;
future<void> fu1 = async(fun1, ref(num));
future<void> fu2 = async(fun2, ref(num));
cout << "Main thread's num:" << num << endl;
return 0;
}
//因为主线程和th1子线程和th2子线程都异步执行,且并没有对每一个线程设置互斥锁,导致num这一临界资源被多个线程修改,呈现出0、10、20的结果。
std::future 是用来获取异步操作结果的模板类;std::packaged_task, std::promise, std::async 都可以进行异步操作,并拥有一个 std::future 对象,用来存储它们所进行的异步操作返回或设置的值(或异常),这个值会在将来的某一个时间点,通过某种机制被修改后,保存在其对应的 std::future 对象中:
对于 std::promise,可以通过调用 std::promise::set_value 来设置值并通知 std::future 对象:
栗子🌰_2:
class Foo {
promise<void> pro1, pro2;
public:
void first(function<void()> printFirst) {
printFirst();
pro1.set_value();
}
void second(function<void()> printSecond) {
pro1.get_future().wait();
printSecond();
pro2.set_value();
}
void third(function<void()> printThird) {
pro2.get_future().wait();
printThird();
}
};
PS:std::future::wait 和 std::future::get 都会阻塞地等待拥有它的 promise 对象返回其所存储的值,后者还会获取 T 类型的对象;这道题只需要利用到异步通信的机制,所以并没有返回任何实际的值。
std::packaged_task 是一个拥有 std::future 对象的 functor,将一系列操作进行了封装,在运行结束之后会将返回值保存在其所拥有的 std::future 对象中;同样地,在这道题中只需要利用到其函数运行结束之后通知 std::future 对象的机制:
栗子🌰_3:
class Foo {
function<void()> task = []() {};
packaged_task<void()> pt_1{ task }, pt_2{ task };
public:
void first(function<void()> printFirst) {
printFirst();
pt_1();
}
void second(function<void()> printSecond) {
pt_1.get_future().wait();
printSecond();
pt_2();
}
void third(function<void()> printThird) {
pt_2.get_future().wait();
printThird();
}
};
6. 条件变量
条件变量一般和互斥锁搭配使用,互斥锁用于上锁,条件变量用于在多线程环境中等待特定事件发生。
针对这道题我们可以分别在 first 和 second 执行完之后修改特定变量的值(例如修改成员变量 k 为特定值),然后通知条件变量,唤醒下一个函数继续执行。
std::condition_variable 是一种用来同时阻塞多个线程的同步原语(synchronization primitive),std::condition_variable 必须和 std::unique_lock 搭配使用:
栗子🌰_1:
class Foo {
condition_variable cv;
mutex mtx;
int k = 0;
public:
void first(function<void()> printFirst) {
printFirst();
k = 1;
cv.notify_all(); // 通知其他所有在等待唤醒队列中的线程
}
void second(function<void()> printSecond) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this](){ return k == 1; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 1 才能继续运行
printSecond();
k = 2;
cv.notify_one(); // 随机通知一个(unspecified)在等待唤醒队列中的线程
}
void third(function<void()> printThird) {
unique_lock<mutex> lock(mtx); // lock mtx
cv.wait(lock, [this](){ return k == 2; }); // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 2 才能继续运行
printThird();
}
};
std::condition_variable::wait 函数会执行三个操作:先将当前线程加入到等待唤醒队列,然后 unlock mutex 对象,最后阻塞当前线程;它有两种重载形式,第一种只接收一个 std::mutex 对象,此时线程一旦接受到唤醒信号(通过 std::condition_variable::notify_one 或 std::condition_variable::notify_all 进行唤醒),则无条件立即被唤醒,并重新 lock mutex;第二种重载形式还会接收一个条件(一般是 variable 或者 std::function),即只有当满足这个条件时,当前线程才能被唤醒,它在 gcc 中的实现也很简单,只是在第一种重载形式之外加了一个 while 循环来保证只有在满足给定条件后才被唤醒,否则重新调用 wait 函数
相关参考文献:
C++多线程编程_Nine-days的博客-优快云博客_#include <thread.h> #include <thread>
https://www.cnblogs.com/lx17746071609/p/11128255.html