4 线程同步与数据访问
存在问题
多个线程共享资源出现访问冲突
- 读读互补冲突
- 读写冲突
- 写写冲突
解决问题的方法
保证操作的原子性和次序。atomicity不可分割。order按次序执行。
- future和promise能够保证原子性和次序。一定是在形成返回值和异常后,future才会读取数据,否则进行堵塞。
- mutex和lock
- condition variable
- atomic data type底层接口
4.1 mutex和lock
mutex简单说明mutex
函数 | 作用 |
---|---|
lock | 锁定互斥,若互斥不可用则阻塞 |
try_lock | 尝试锁定互斥,若互斥不可用则返回 |
unlock | 解锁互斥 |
int val ;
mutex valMutex;
valMutex.lock();
//val的访问和修改
valMutex.unlock();
- 每次访问前上锁。访问后开锁。
- 如果其他程序已经上锁,那么当前程序阻塞,直到其他程序释放锁。(发送开锁信号激活)
- 存在的问题:中途出现异常,无法执行开锁。资源会被永久上锁。
- mutex尝试锁try_lock()用来判断资源是否上锁。如果成功就返回true,此时调用可以上锁
mutex m;
while(m.try_lock()==false){
doSomethingOthers();
}
lock_guard<mutex> lg(m,adopt_lock);
mutex递归锁recursive_mutex
- recursive_mutex与mutex操作完全一致。
- 死锁:两个程序分别锁上了对方需要的资源,并在相互等待。
- 递归锁:一个线程两次上锁,导致第二次上锁的时候资源被自己占用。也是一种死锁。
- recursive_mutex 能够防止递归锁出现。即防止同一个线程多次上锁同一个资源
mutex时间锁timed_mutex/recursive_time_mutex
函数 | 作用 |
---|---|
lock | 锁定互斥,若互斥不可用则阻塞 |
try_lock | 尝试锁定互斥,若互斥不可用则返回 |
try_lock_for | 尝试锁定互斥,若互斥在指定的时限时期中不可用则返回 |
try_lock_until | 尝试锁定互斥,若直至抵达指定时间点互斥不可用则返回 |
(公开成员函数) | |
unlock | 解锁互斥 |
- 等待某个时间段。返回是否上锁。有如下成员函数
try_lock_for()
try_lock_until()
mutex进阶版本lock_guard
std::lock_guard<std::mutex> sbguard1(my_mutex1, std::adopt_lock);// std::adopt_lock标记作用;
- 使用lock_guard管理锁。这样当出现异常后,lock_guard局部变量被销毁,执行析构函数的时候回自动释放资源锁。
- lock_guard的第二个标质量adopt_lock标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);通知lock_guard不需要再构造函数中lock这个互斥量了。
//mute & lock
#include<future>
#include<mutex>
#include<iostream>
#include<string>
using namespace std;
//互斥体的控制变量
mutex printMutex;
void print(const std::string&s){
// 如果没有枷锁,多个线程共同调用会乱序输出
lock_guard<mutex> l(printMutex);
for(char c:s){
cout.put(c);
}
cout<<endl;
}
int main()
{
auto f1 = async(print,"Hello from a first thread");
auto f2 = async(print,"hello from a second thread");
print("hello from the main thread");
return 0;
}
mutex包装器unique_lock
- 加强版的lock_guard。保存一个mutex对象。可以只是用这一个函数解决所有问题。
- lock_gurad只能在析构函数中解锁,无法在同一个线程中进行细粒度的控制。但是unique_lock可以自己加锁解锁。
函数 | 作用 |
---|---|
lock | 锁定关联互斥 |
try_lock | 尝试锁定关联互斥,若互斥不可用则返回 |
try_lock_for | 试图锁定关联的可定时锁定 (TimedLockable) 互斥,若互斥在给定时长中不可用则返回 |
try_lock_until | 尝试锁定关联可定时锁定 (TimedLockable) 互斥,若抵达指定时间点互斥仍不可用则返回 |
unlock | 解锁关联互斥 |
release | 将关联互斥解关联而不解锁它.返回unique_lock所有的锁的指针。可以自己解锁 |
mutex | 返回指向关联互斥的指针 |
owns_lock | 测试锁是否占有其关联互斥 |
operator bool | 测试锁是否占有其关联互斥 |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cvrsbEOC-1691759243705)(image/2021-03-08-18-31-27.png)]
void shared_print(string msg, int id) {
std::unique_lock<std::mutex> guard(_mu);
//do something 1
guard.unlock(); //临时解锁
//do something 2
guard.lock(); //继续上锁
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
// 结束时析构guard会临时解锁
// 这句话可要可不要,不写,析构的时候也会自动执行
// guard.ulock();
}
4.2 condition variable
简介
- future的目的是处理线程的返回值和异常。因为它只能携带一次数据返回。
- 这个明显是解决生产者和消费者问题。或者读、写问题。因为资源有数量限制。而之前的mutex只有互斥限制,也就是说,mutex与lock只能控制数量为1的消费者互斥访问问题。
- condition variable控制数量大于1 的生产和消费问题
condition_variable原理
-
condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。
-
有意修改变量的线程必须
- 获得 std::mutex (常通过 std::lock_guard )
- 在保有锁时进行修改
- 在 std::condition_variable 上执行 notify_one 或 notify_all (不需要为通知保有锁)
-
即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。
-
任何有意在 std::condition_variable 上等待的线程必须
- 在与用于保护共享变量者相同的互斥上获得 std::unique_lockstd::mutex
- 执行下列之一:
- 检查条件,是否为已更新或提醒它的情况
- 执行 wait 、 wait_for 或 wait_until ,等待操作自动释放互斥,并悬挂线程的执行。
- condition_variable 被通知时,时限消失或虚假唤醒发生,线程被唤醒,且自动重获得互斥。之后线程应检查条件,若唤醒是虚假的,则继续等待。
- 或者,使用 wait 、 wait_for 及 wait_until 的有谓词重载,它们包揽以上三个步骤
condition_variable的消费者。有一下三种情况。
- 当资源没有被生产出来,没有加锁时,加锁,wait(),解锁,等待通知。
- 当资源被锁时,在unique_lock处等待解锁。
- 当资源生产出来,没有加锁时,直接执行。
对于第一种情况:condition_varaiblewait操作能够解锁等待信号量。当信号量来到时,加锁执行操作,然后解锁,退出。当信号量来到时,加锁,但是第二个参数的内容发现是虚假信号,能够继续解锁等待信号量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TDT4830s-1691759243706)(image/2021-03-08-19-36-00.png)]
condition_variable操作
函数 | 作用 |
---|---|
notify_one | 通知一个等待的线程 |
notify_all | 通知所有等待的线程 |
wait | 阻塞当前线程,直到条件变量被唤醒 |
wait_for | 阻塞当前线程,直到条件变量被唤醒,或到指定时限时长后 |
wait_until | 阻塞当前线程,直到条件变量被唤醒,或直到抵达指定时间点 |
使用条件
- 一个“存放数据”的对象,或一个“表示条件满足”的flag。此处的readyFlag
- 一个mutex对象,此处的readyMutex
- 一个condition_variable对象词的readyCondVar
编程实现——简单使用
//condition variable生产者消费者问题
#include<condition_variable>
#include<mutex>
#include<future>
#include<iostream>
using namespace std;
bool readyFlag;
mutex readyMutex;
condition_variable readyCondVar;
void thread1(){
cout<<"thread1"<<endl;
cin.get();
//以下是lock保护区
{
lock_guard<mutex> lg(readyMutex);
readyFlag = true;
}
readyCondVar.notify_one();
}
void thread2(){
{
unique_lock<mutex> ul(readyMutex);
readyCondVar.wait(ul,[]{return readyFlag;});
}
cout<<"done"<<endl;
return;
}
int main(){
auto f1 = async(thread1);
auto f2 = async(thread2);
}
编程实现——多线程Queue
- 生产者和消费者问题
//condition variable实现多线程queue
#include<condition_variable>
#include<mutex>
#include<future>
#include<thread>
#include<iostream>
#include<queue>
using namespace std;
queue<int> que;//消费对象
mutex queueMutex;
condition_variable queueCondVar;
//生产者
void provider(int val){
for(int i=0;i<6;++i){
lock_guard<mutex> lg(queueMutex);
que.push(val+i);
//貌似这句话会被优化掉
this_thread::sleep_for(chrono::microseconds(100000));
}
queueCondVar.notify_one();
this_thread::sleep_for(chrono::microseconds(val));
}
//消费者
void consumer(int num){
while(true){
int val;
{
unique_lock<mutex> ul(queueMutex);
queueCondVar.wait(ul,[]{return !que.empty();});
val = que.front();
que.pop();
cout<<"consumer"<<num<<":"<<val<<endl;
}
}
}
int main()
{
//生产者列表
auto p1 = async(provider,1000);
auto p2 = async(provider,2000);
auto p3 = async(provider,3000);
//消费者列表
auto c1 = async(consumer,1);
auto c2 = async(consumer,2);
}
4.3 atomic data
等到以后再写吧。感觉没有必要。
5 this_thread
函数 | 作用 |
---|---|
get_id | 获得thread id (function ) |
yield | 放弃执行 (function ) |
sleep_until | 休眠到某个时间节点chrono::timepoint (function ) |
sleep_for | 休眠某个时间段chrono::duration (function ) |