1.多线程初步
1.包含的库
#Include<thread>
2.涉及到的类 std::thread(这个类是属于标准模版库的,底层封装的系统调用)
3.代码实例
#include <iostream>
#include <thread>
void hello(){ std::cout << "Hello World" << std::endl;
}
int main(){std::thread t(hello);
// std::thread thread{ [] {std::cout << "Hello World!\n"; } };t.join();
return 0;
}
当线程函数含有参数的时候,将参数写在后面即可
void print(int x) { std::cout << x;
}
int main() {int value = 42;thread t(print,value);
t.join();return 0;
}
特殊例子,当参数传参方式为引用时
#include <iostream>
#include <thread>
#include <functional> void increment(int& x) { x*=2;
}
int main() {int value = 42;
//补全代码std::thread t(increment,std::ref(value));
std::cout<<value; //期望是84return 0;
}
2.共享数据与线程同步
1.互斥量的介绍
当我们有多个线程任务对同一块共享数据进行读写时,为了协调这些线程对共享数据的访问,我们进入互斥量这一概念,在C++中,也就是锁:std::mutex。
当第一个线程执行到lock的时候,它拿到cpu的执行权,继续执行线程逻辑。当另外的线程执行到lock的时候,它会暂时陷入自旋,此时它会独占一个线程通道,自选期间如果lock的线程执行了unlock它就可以继续执行下去。但是如果lock的线程迟迟执行不到unlock,那么操作系统是不会允许这个线程通道一直被这个自旋的线程独占,这时候会从用户态转入内核态,让出线程通道来让别的线程执行。
还有一种不需要进入内核态的锁,是std::atomic ,原子变量的加速无需经过操作系统的处理,是通过编译器用原子指令实现的,所以原子量的效率优于线程锁。
2.关于粒度
锁本身不会造成严重的性能开销,但是加锁不当会导致程序转入内核态,这个过程有巨大的性能开销,所以我们在写C++代码的时候除了要注意共享数据的安全性,也要注意程序的性能,核心要点就是不要让一个线程陷入长时间的等待。
程序在哪些关键步骤需要加锁被称之为锁的粒度,如果程序上锁了无需上锁的语句,在逻辑和功能上看起来没有任何问题,但是却会导致性能上面的问题,所以上锁的时候我们首先应该关心锁的正确性,其次就是关心锁的粒度,既在一个多线程环境下,哪些步骤是必须需要加锁的,哪些步骤无需加锁。
3.lock_guard
- lock_guard 是一个RAII(资源获取即初始化)风格的锁管理类,用于自动管理互斥锁(mutex)的加锁和解锁。
- 它在构造时锁定互斥锁,在析构时自动释放锁,确保即使发生异常也不会忘记解锁。
注意:lock_guard不支持手动lock()和unlock(),通常与{}结合来控制其生命期,进而控制临界区
4.thread_local
- thread_local 是一个存储类说明符,用于声明线程局部变量
- 每个线程都有其独立的变量副本,线程之间不会共享这些变量。
注:访问thread_local的开销比普通变量大
实例代码
#include<iostream>
#include<thread>
#include<mutex>thread_local int tls_var=0;
std::mutex m;
void thread_func(int id){tls_var=id;std::lock_guard<std::mutex>lg(m);//锁的自动释放和上锁std::cout<<"thread id="<<std::this_thread::get_id()<<", tls_var="<<tls_var<<std::endl;
}
void case05(){std::thread t1(thread_func,1);std::thread t2(thread_func,2);t1.join();t2.join();
}
5.unique_lock
- unique_lock 是一个更灵活的锁管理类,与lock_guard类似,但提供了更多功能。
- 它支持手动加锁、解锁、延迟加锁、条件变量等高级功能。
代码实例
6.关于死锁
两个或多个线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。
例:比如我们有两个线程代表两台服务器,一个服务器用于数据查询存储,一台用于数据计算。然后我们日常生活中有两种任务,一种任务是先查询数据,然后使用查询到的数据进行计算。另一种是先计算数据,然后将计算结果保存。这时候如果我们仅仅对每台服务器单独加锁,就会出现一种场景,当两种任务同时到来的时候,任务A执行完成想要获取计算服务器,但是计算服务器被任务B占用;任务B想要获取查询存储服务器但是被任务A占用。
解决死锁总的来说有两种解法:一种是我们不对可能出现冲突的局部加锁,而是对整个任务加锁。就像上面的例子中,当任务A来的时候,同时对数据服务器和计算服务器加锁,计算完成之后释放。这样就不会出现死锁。
另一种解法是当一个任务持有锁超时之后就释放任务,短暂等待后重新执行任务。
问题:这两种解法分别适用于什么场景?
7.关于条件变量
日常生活中有一类常见的问题,那就是生产者消费者问题,这类型问题几乎无处不在。比如我们观看在线视频。这里有一个逻辑就是视频的数据来源于网络,数据的使用者是本地播放器。因此这里就有一个模型,即只有下载够一定数量的数据的时候播放器才开始播放,当下载数据超过一个限度的时候就停止缓存,当播放(使用数据)到一定程度的时候又继续下载。
这个模型里下载器扮演了生产者的角色,播放器扮演了消费者的角色。
实例代码:
情景
我们假设一个场景,有一个下载线程和一个播放线程,下载线程检测到当前没有任何数据就开始下载,直到下载数据数量达到5(我们假设一次下载任务执行可以下载一个数据),下载数量达到5之后下载线程停止工作,播放线程开始工作,直到消耗完下载线程的所有数据。请用C++代码表示这个过程。
我们假设上面的例子不是一次性的,而是有一个场景,在这个场景下我们这两个线程长期存在,线程A会一致下载文件到本地磁盘中,当线程A下载文件大小占满磁盘空间时候,我们就暂停下载并且开启B线程,B线程会解析处理这些文件,然后将其删除,当B线程删除文件到磁盘空间为0的时候,我们就暂停B线程并重新启动A线程。
std::mutex mut;
std::condition_variable cond_var;int disk_capacity = 3;std::mutex print_mutex;
void safe_print(const std::string& msg)
{std::lock_guard<std::mutex> guard(print_mutex);std::cout << msg << std::endl;
}void download_file()
{std::unique_lock<std::mutex> lock(mut);while (true){cond_var.wait(lock,[]{return disk_capacity ;});while (disk_capacity){std::stringstream ss;ss << "files download ready, disk_capacity is " << disk_capacity;std::string formatted_string = ss.str();safe_print(formatted_string);std::this_thread::sleep_for(std::chrono::seconds(1));disk_capacity--;}cond_var.notify_all();}
}void get_files_do_something()
{std::unique_lock<std::mutex> lock(mut);while (true){cond_var.wait(lock, [] {return disk_capacity == 0;});//...解析文件,并且将磁盘清空disk_capacity = 3;std::stringstream ss;ss << "files delete, disk_capacity is " << disk_capacity << "-----" << std::this_thread::get_id();std::string formatted_string = ss.str();safe_print(formatted_string);cond_var.notify_all();}
}int main()
{std::thread t1(get_files_do_something);std::thread t2(download_file);std::thread t3(get_files_do_something);t1.join();t2.join();t3.join();return 0;
}
对于条件变量的补充:
1.几个重要方法
- std::condition_variable::wait(...)如果谓词返回为true,则线程继续运行,返回为false,则线程阻塞,并且释放锁
- std::condition_variable::notify_all() 唤醒其它所有线程,尝试拿锁,如果拿到锁,则做谓词条件判断,注意,此操作并不会使线程释放锁
实例代码补充:
第二版
#include<iostream>
#include<thread>
#include<mutex>
#include<queue>
#include<condition_variable>
/*
producer and consumer problem
*/
const int N=5;
std::queue<int>buffer;
std::mutex mtx;
std::condition_variable con_var;
void producer(){std::unique_lock<std::mutex>lock(mtx);while(1){con_var.wait(lock,[](){return buffer.size()==0;});for(int i=0;i<N;i++){buffer.push(i+1);std::cout<<"producer produce data:"<<i+1<<std::endl;}std::cout<<std::endl;std::this_thread::sleep_for(std::chrono::seconds(2)); con_var.notify_all();}
}
void consumer(int id){while(1){{std::unique_lock<std::mutex>lock(mtx);con_var.wait(lock,[](){return !buffer.empty(); });int data=buffer.front();buffer.pop();std::cout<<"consumer:"<<id<<" consume data:"<<data<<std::endl;if(buffer.empty()){std::cout<<std::endl;con_var.notify_all();} }std::this_thread::sleep_for(std::chrono::seconds(2));//超级底层,要是不做细微延时的话,会出现独占锁的情况}
}
void case06(){std::thread t1(producer);std::thread t2(consumer,1);std::thread t3(consumer,2);std::thread t4(consumer,3);std::thread t5(consumer,5);t1.join();t2.join();t3.join();return;
}
实例代码补充结果如下:
可以看到,当数据队列(共享资源)为空时,生产者线程一次性写5个数据,唤醒所有消费者线程;当数据队列不为空时,5个消费者线程并发的消费数据,当数据被消耗殆尽时,唤醒所有线程(其实旨在让生产者拿到锁,进入活动)