1.互斥锁mutex
行为:当多个线程竞争同一把锁时,首先会执行加锁操作,如果加锁失败,也就是没有获取锁,线程会进入阻塞态,阻塞在加锁操作上,如果有其他线程释放锁,也就是锁可用了,在linux中,由内核决定唤醒一个线程而让他们直接获得锁。
注意:如果阻塞在加锁操作上,是不用用户线程显式执行唤醒操作的。
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void func(){
std::cout<<"threadid="<<std::this_thread::get_id()<<"try get lock "<<std::endl;
//使用智能锁unique_lock 默认创建智能锁自动执行加锁操作
//加锁范围是 对象创建这一行到出作用域析构
std::unique_lock<std::mutex> lck(mtx_);
std::cout<<"threadid="<<std::this_thread::get_id()<<"get lock "<<std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout<<"thread:id="<<std::this_thread::get_id()<<"release lock "<<std::endl;
}
};
int main(){
Thread myThread;//绑定器成员函数的调用依赖一个对象 保证子线程访问的都是同一个对象的锁和条件变量
std::vector<std::unique_ptr<std::thread>> threads;
//创建三个线程并设置分离线程
for (int i = 0; i < 3; ++i) {
threads.emplace_back(std::make_unique<std::thread>(std::bind(&Thread::func,&myThread)));
}
for (int i = 0; i < threads.size(); ++i) {
threads[i]->detach();
}
//等待子线程执行完毕
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
执行结果分析:
threadid=threadid=3try get lock threadid=4try get lock
threadid=4get lock2try get lock
thread:id=4release lock
threadid=3get lock
thread:id=3release lock
threadid=2get lock
thread:id=2release lock
可以看出在获取锁之前的操作是并发乱序执行的,之后线程4获得了锁,线程2 3被阻塞,两秒后释放了锁,然后线程3被操作系统唤醒获取了锁,最后是线程2被操作系统唤醒获取了锁,整个过程用户没有执行显示的notify操作,因此可以得知,阻塞在加锁操作的线程,是由操作系统唤醒的,不需要用户干预。
2.条件变量condition_variable
条件变量最主要的就是两大操作:1.notify2.wait。其中notify操作比较简单,无论是cv.notify_all()还是cv.notify_one(),两个操作仅仅是负责唤醒阻塞在条件变量cv上的线程;对于wait根据是否指定超时时间,可以分为wait和wait_for(先不考虑wait_until),根据是否向wait传递等待条件,wait一共有四种方式:wait(lock),wait(lock,condition),wait_for(lock,time),wait_for(lock,time,condition)其中这四种方式行为各不相同下面将会做出详细说明。
2.1cv.notify_all()/cv.notify_one()
cv.notify_all()是唤醒等待条件变量cv上的所有线程,cv.notify_one()是唤醒等待在条件变量cv上的一个线程,这个不是讨论的重点。仅以cv.notify_all()为例。
注意:cv.notify操作会唤醒等待在条件变量cv上的线程,不涉及任何和锁相关的操作,notify不获取锁,也不释放锁,一个线程即使没有获得锁也能执行notify操作。
注意:一个调用cv.wait(lock)被阻塞的线程(先不考虑调用cv.wait_for设置了超时时间的情况),只能等待用户其他线程显示调用这个条件变量的notify操作来唤醒这个线程,不会自己苏醒;线程阻塞在条件变量的wait操作和加锁操作是不同的,阻塞在加锁操作行为已经说过,不再赘述。
演示代码1
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void funcWait(){
std::unique_lock<std::mutex> lck(mtx_);
cv_.wait(lck);
std::cout<<"threadid="<<std::this_thread::get_id()<<"wait end"<<std::endl;
}
void funcLockAndRelease(){
std::unique_lock<std::mutex> lck(mtx_);
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout<<"threadid="<<std::this_thread::get_id()<<"sleep end"<<std::endl;
std::cout<<"threadid="<<std::this_thread::get_id()<<"release lock"<<std::endl;
}
};
int main(){
Thread myThread;
std::thread t1(std::bind(&Thread::funcWait,&myThread));
t1.detach();
//保证t1先执行
std::this_thread::sleep_for(std::chrono::seconds(1));
std::thread t2(std::bind(&Thread::funcLockAndRelease,&myThread));
t2.detach();
//等待子线程执行完毕
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
执行结果分析:
threadid=3sleep end
threadid=3release lock
我们让线程1先执行然后cv_.wait(lck)让线程1释放锁,然后线程2获得了锁,执行了自己的任务,然后释放了锁,可以看到线程1仍然在阻塞,并没有看到wait end字样,但是此时锁是可用的,说明,即使锁可用,没有显式调用cv_.notify的话,阻塞在条件变量上的线程不会被唤醒,印证了上述结论。
演示代码2
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void funcNotify(){
std::unique_lock<std::mutex> lck(mtx_);
cv_.notify_all();
std::cout<<"threadid="<<std::this_thread::get_id()<<" notify "<<std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout<<"thread:id="<<std::this_thread::get_id()<<" release lock "<<std::endl;
}
void funcWait(){
std::unique_lock<std::mutex> lck(mtx_);
cv_.wait(lck);
std::cout<<"threadid="<<std::this_thread::get_id()<<" wait end"<<std::endl;
}
};
int main(){
Thread myThread;
std::thread t1(&Thread::funcWait,&myThread);
//保证t1先执行
std::this_thread::sleep_for(std::chrono::seconds(1));
std::thread t2(&Thread::funcNotify,&myThread);
t1.detach();
t2.detach();
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
执行结果分析:
threadid=3 notify
thread:id=3 release lock
threadid=2 wait end
执行结果是先打印第一行,休眠三秒后相继打印第二行第三行。执行过程首先是线程1执行funcWait线程函数,获得了锁,调用wait释放了锁并且将自己阻塞;线程2执行线程函数funcNotify,首先是获得了锁,然后进行cv.notify_all()操作,唤醒线程1,此时线程1已经苏醒,但是线程1并未向下执行,这是由于虽然线程1苏醒了,还没有获得锁不能向下执行,锁仍然被线程2持有,因此notify不获取锁,也不释放锁,锁的释放仍然要等线程2执行完毕出作用域。因此线程2在notify之后仍然能执行线程2后续代码,直至执行完毕释放锁,然后线程1获得锁,线程1继续执行。
演示代码3
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void funcNotify(){
cv_.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout<<"thread:id="<<std::this_thread::get_id()<<" notify end"<<std::endl;
}
void funcWait(){
std::unique_lock<std::mutex> lck(mtx_);
cv_.wait(lck);
std::cout<<"threadid="<<std::this_thread::get_id()<<" wait end"<<std::endl;
}
};
int main(){
Thread myThread;
std::thread t1(&Thread::funcWait,&myThread);
//保证t1先执行
std::this_thread::sleep_for(std::chrono::seconds(1));
std::thread t2(&Thread::funcNotify,&myThread);
t1.detach();
t2.detach();
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
相较于演示代码2,只是线程函数funcNotify稍作修改,不进行加锁操作,仅仅是notify。
执行结果分析:
threadid=2 wait end
thread:id=3 notify end
执行流程:线程1先执行,获得锁,然后调用cv.wait()释放锁,将自己阻塞,然后线程2执行cv.notify_all(),唤醒等待在条件变量cv上的线程。注意:此时锁是空闲的,所以当执行完cv.notify_all被唤醒后,线程1直接被唤醒拿到锁,直接开始执行,打印threadid=2 wait end线程1结束,此时线程2还未结束。再次印证了notify不获取锁,也不释放锁,一个线程即使没有获得锁也能执行notify操作
2.2cv.wait(lock)
首先讨论wait的第一个重载版本,只传入一个unique_lock。如果是第一次执行到cv.wait(lock)那么线程会无条件释放互斥锁,并且阻塞当前线程;如果线程阻塞在cv.wait(lock)的话,唯一打破阻塞的方法就是等待其他线程调用cv.notify进行唤醒,否则会一直阻塞;这个线程一旦被唤醒,就不会再次阻塞(只调用一次cv.wait的情况下,无循环),他会一直竞争,直到获得锁,然后执行后续代码。
演示代码
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void funcNotify(){
cv_.notify_all();
std::cout<<"thread:id="<<std::this_thread::get_id()<<" notify "<<std::endl;
}
void funcWait(){
std::cout<<"threadid="<<std::this_thread::get_id()<<" try get lock "<<std::endl;
std::unique_lock<std::mutex> lck(mtx_);
cv_.wait(lck);
std::cout<<"threadid="<<std::this_thread::get_id()<<" start!!! "<<std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout<<"threadid="<<std::this_thread::get_id()<<" end!!! "<<std::endl;
}
};
int main(){
Thread myThread;
std::vector<std::unique_ptr<std::thread>> threads;
for (int i = 0; i < 3; ++i) {
threads.emplace_back(std::make_unique<std::thread>(std::bind(&Thread::funcWait,&myThread)));
}
for (int i = 0; i < threads.size(); ++i) {
threads[i]->detach();
}
std::this_thread::sleep_for(std::chrono::seconds(2));
std::thread t(std::bind(&Thread::funcNotify,&myThread));
t.detach();
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
执行结果分析:
threadid=threadid=4 try get lock threadid=3 try get lock
2 try get lock
thread:id=5 notify
threadid=2 start!!!
threadid=2 end!!!
threadid=4 start!!!
threadid=4 end!!!
threadid=3 start!!!
threadid=3 end!!!
首先让线程id为2,3,4的线程阻塞,然后线程id为5的线程负责唤醒他们,之后线程2,3,4苏醒;线程竞争获得互斥锁,即使第一次没有获取锁,也不会再次阻塞,而是继续竞争,直到获取锁。
2.3cv.wait(lock,condition)
cv.wait(lock)只阻塞一次,也只需要唤醒一次;但是加了条件就不一样了,可能需要阻塞多次,并且唤醒多次。第一次调用cv.wait(lock,condition)如果条件不满足的话,释放锁,并阻塞当前线程;直至被其他线程调用cv.notify显式唤醒,唤醒后仍然是先尝试获取锁,获取锁了以后,会再次判断条件(没有处于阻塞态,获取锁以后才会判断条件),如果条件满足会向下执行,反之会再次释放锁并阻塞线程。
演示代码
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void funcNotify(){
int count=0;
while (count++<10){
std::this_thread::sleep_for(std::chrono::seconds(1));
cv_.notify_all();
std::cout<<" notify "<<std::endl;
}
}
void funcWait(){
std::unique_lock<std::mutex> lck(mtx_);
int count=0;
cv_.wait(lck,[&]()->bool{
count++;
std::cout<<" check condition count: "<<count<<std::endl;
return count>5;
});
std::cout<<" start!!! "<<std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
std::cout<<" end!!! "<<std::endl;
}
};
int main(){
Thread myThread;
std::thread t1(std::bind(&Thread::funcWait,&myThread));
t1.detach();
std::this_thread::sleep_for(std::chrono::seconds(2));
std::thread t2(std::bind(&Thread::funcNotify,&myThread));
t2.detach();
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
执行结果分析:
check condition count: 1
notify
lock release
check condition count: 2
notify
lock release
check condition count: 3
notify
lock release
check condition count: 4
notify
lock release
check condition count: 5
notify
lock release
check condition count: 6
start!!!
end!!!
notify
lock release
notify
lock release
notify
lock release
notify
lock release
notify
lock release
线程2执行notify之后,未立刻释放锁;线程1虽然会被唤醒但没有立刻获得锁,线程一也是在线程2释放锁之后才去检查条件。印证了cv.wait(lock,condition)可能会阻塞多次直到条件满足,而且只在没有被阻塞,且获得锁以后,才会进行条件判断。
使用cv.wait(lock,condition)和cv.wait(lock)+循环的行为是一样的 如下
cv_.wait(lck,[&]()->bool{
count++;
std::cout<<" check condition count: "<<count<<std::endl;
return count>5;
});
while(count<=5){
cv_.wait(lck);
count++;
std::cout<<" check condition count: "<<count<<std::endl;
}
2.4cv.wait_for(lock,time)
相较于cv.wait(lock),cv.wait_for(lock,time)能够设置一个超时时间。如果超时时间到期的话,线程不需要借助其他线程显式调用cv.notify,能自动唤醒,并尝试获取锁,获取锁之后,继续执行后续代码。可根据返回值,查看是否是超时返回。
演示代码
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void funcWait(){
std::unique_lock<std::mutex> lck(mtx_);
std::cout<<"begin!!! "<<std::endl;
if(std::cv_status::timeout==cv_.wait_for(lck,std::chrono::seconds(5))){
std::cout<<"timeout "<<std::endl;
}else{
std::cout<<"notimeout "<<std::endl;
}
std::cout<<"end!!! "<<std::endl;
}
};
int main(){
Thread myThread;
std::thread t(std::bind(&Thread::funcWait,&myThread));
t.detach();
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
执行结果分析:
begin!!!
timeout
end!!!
先打印begin!!!,5s后打印后两行。在没有其他线程唤醒的情况下,超时时间到期了确实是摆脱了阻塞状态;实际上5s到期后线程会自己苏醒,但是不一定会立即执行后续代码,而是获取了锁才能继续执行,因此在有锁竞争的场景下,不一定5s到了就能向下执行,还需要花费额外的时间获取锁。
实际应用中如果你需要让某个正在阻塞的线程完成某个任务,可以选择外部显式notify唤醒线程,或者设置超时时间让它自己苏醒。
2.5cv.wait_for(lock,time,condition)
相较于cv.wait(lock,condition)如果在不超时的情况下,二者行为一样,如果发生超时和cv.wait_for(lock,time)的行为一样,也是将自己唤醒,然后尝试获取锁,但是会执行condition里的代码,但是无论返回值是true还是false,都会执行后续代码。实际上,如果想用wait_for,并且需要循环判断的话更倾向使用while+cv.wait_for(lock,time)。
演示代码
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>
#include <functional>
#include <chrono>
#include <vector>
#include <memory>
class Thread{
public:
std::mutex mtx_;
std::condition_variable cv_;
void funcWait(){
std::unique_lock<std::mutex> lck(mtx_);
int count=0;
cv_.wait_for(lck,std::chrono::seconds(6),[&]()->bool{
count++;
std::cout<<count<<" times "<<std::endl;
return false;
});
std::cout<<count<<" end!!! "<<std::endl;
}
};
int main(){
Thread myThread;
std::thread t(std::bind(&Thread::funcWait,&myThread));
t.detach();
std::this_thread::sleep_for(std::chrono::seconds(100));
return 0;
}
执行结果分析:
1 times
2 times
2 end!!!
第一次进入时条件不满足,释放锁,并阻塞;6s后将自己唤醒,获得互斥锁之后,继续执行判断条件里的代码,由于是超时返回,故即使条件不成立也不会再次阻塞。