C++复习之(并发多线程)
1.多线程好处
提高cpu利用率,若只有一个线程,当这个线程阻塞后整个活动停止;若有多个线程分别完成不同的任务,当一个线程被阻塞,另外的线程也能继续工作,从而提高cpu的利用率。 当性能的增幅比不上成本时,多线程就没有那么好了。运行越多的线程,操作系统需要为每个线程分配独立的栈空间,需要越多的上下文切换,这会消耗很多操作系统资源,如果在线程上的任务完成得很快,那 么实际执行任务的时间要比启动线程的时间小很多,所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能,此时收益就比不上成本。
2.线程的创建
使用std::thread对象来创建一个线程,并传入该线程需要执行的函数。在主函数中,使用join()函数来等待线程结束,代码例子如下。
线程创建与回收
#include<iostream>
#include<thread>
using namespace std;
void proc(int &a)
{
cout << "我是子线程,传入参数为" << a << endl;
cout << "子线程中显示子线程id为" << this_thread::get_id()<< endl;
}
int main()
{
cout << "我是主线程" << endl;
int a = 9;
thread th2(proc,ref(a));//第一个参数为函数名,第二个参数为该函数的第一个参数,如果该函数接收多个参数就依次写在后面。此时线程开始执行。
cout << "主线程中显示子线程id为" << th2.get_id() << endl;
th2.join();//此时主线程被阻塞直至子线程执行结束。
return 0;
}
3. 互斥锁的使用
3.1 互斥锁作用与临界区、信号量区别
互斥锁目的保证线程安全和防止死锁。当一个线程在使用某个互斥资源,需要对资源上锁,从而解决访问冲突。当两个变量在使用同一个共享变量时,可能会发生数据混乱,从而发生程序错误。
临界区:速度最快,但只能作用于同一进程下不同线程,不能作用于不同进程;临界区可确保某一代码段同一时刻只被一个线程执行;
信号量:多个线程同一时刻访问共享资源,进行线程的计数,确保同时访问资源的线程数目不超过上限,当访问数超过上限后,不发出信号量;
互斥锁:比临界区慢,但支持不同进程间的同步与互斥,互斥就是保证资源同一时刻只能被一个进程使用;
3.2 读写锁的使用
创建一个shared_mutex对象,然后使用lock_shared(),unlock_shared()加读锁,lock(),unlock()加写锁。可以多个同时读,但只能一个同时写。代码如下:
读写锁
shared_mutex s_m;
std::string book;
void read()
{
s_m.lock_shared();
cout << book;
s_m.unlock_shared();
}
void write()
{
s_m.lock();
book = "new context";
s_m.unlock();
}
3.3 自旋锁
如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,而不是阻塞线程,这减少了线程的上下文切换,节约内核资源,但同时会浪费cpu资源,因此自旋锁适合锁使用者保持锁时间比较短的情况。
3.4 lock_guard 和 unique_lock
直接使用lock()但忘记unlock(),会导致锁无法被释放,使用lock_guard()和unique_lock()则可以避免这个问题。
原理:声明一个局部的std::lock_guard对象,在其构造函数中进行加锁,在其析构函数中进行解锁。创建即加锁,作用域结束自动解锁。从而使用std::lock_guard()就可以替代lock()与unlock()。通过使用{}来调整作用域范围,可使得互斥量m在合适的地方被解锁。
lock_guard
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;//实例化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被解锁
void proc2(int a)
{
{
lock_guard<mutex> g2(m);
cout << "proc2函数正在改写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 t1(proc1, a);
thread t2(proc2, a);
t1.join();
t2.join();
return 0;
}
unique_lock()除了lock_guard()所有功能外,还有try_to_lock()和defer_lock()。try_to_lock():会尝试去锁定,得保证锁处于unlock的状态,然后尝试现在能不能获得锁;尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里,并继续往下执行;defer_lock: 始化了一个没有加锁的mutex。
unique_lock
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
mutex m;
void proc1(int a)
{
unique_lock<mutex> g1(m, defer_lock);//始化了一个没有加锁的mutex
cout << "xxxxxxxx" << endl;
g1.lock();//手动加锁,注意,不是m.lock();注意,不是m.lock(),m已经被g1接管了;
cout << "proc1函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 2 << endl;
g1.unlock();//临时解锁
cout << "xxxxx" << endl;
g1.lock();
cout << "xxxxxx" << endl;
}//自动解锁
void proc2(int a)
{
unique_lock<mutex> g2(m, try_to_lock);//尝试加锁一次,但如果没有锁定成功,会立即返回,不会阻塞在那里,且不会再次尝试锁操作。
if (g2.owns_lock()) {//锁成功
cout << "proc2函数正在改写a" << endl;
cout << "原始a为" << a << endl;
cout << "现在a为" << a + 1 << endl;
}
else {//锁失败则执行这段语句
cout << "" << endl;
}
}//自动解锁
int main()
{
int a = 0;
thread t1(proc1, a);
t1.join();
//thread t2(proc2, a);
//t2.join();
return 0;
}
3.5 condition_variable
3.6 async和future
std::async是一个函数模板,用来启动一个异步任务,它返回一个std::future类模板对象,future对象起到了占位的作用。future.get()只能调用一次。特别的shared_future.get()可以调用多次,因为他们一个转移值的所有权,一个是复制值。
async
#include <iostream>
#include <thread>
#include <mutex>
#include<future>
#include<Windows.h>
using namespace std;
double t1(const double a, const double b)
{
double c = a + b;
Sleep(3000);//假设t1函数是个复杂的计算过程,需要消耗3秒
return c;
}
int main()
{
double a = 2.3;
double b = 6.7;
future<double> fu = async(t1, a, b);//创建异步线程线程,并将线程的执行结果用fu占位;
cout << "正在进行计算" << endl;
cout << "计算结果马上就准备好,请您耐心等待" << endl;
cout << "计算结果:" << fu.get() << endl;//阻塞主线程,直至异步线程return
//cout << "计算结果:" << fu.get() << endl;//取消该语句注释后运行会报错,因为future对象的get()方法只能调用一次。
return 0;
}
3.7 atomic
被atomic修饰的变量具有线程安全,不会有多个线程同时操作一个变量的问题。用来替代频繁加锁解锁问题。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)