互斥锁std::mutex
C++
中常见的cout
是一个共享资源,如果在多个线程同时执行cout
,会发现很奇怪的问题,解决办法就是要对cout
这个共享资源进行保护。
在C++
中,可以使用互斥锁std::mutex
进行资源保护,头文件是#include <mutex>
,共有两种操作:锁定(lock)与解锁(unlock)。
将cout
重新封装成一个线程安全的函数
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
std::mutex mu;
// 使用锁保护
void shared_print(string msg, int id) {
mu.lock(); // 上锁
cout << msg << id << endl;
mu.unlock(); // 解锁
}
void function_1() {
for(int i=0; i>-100; i--)
shared_print(string("From t1: "), i);
}
int main()
{
std::thread t1(function_1);
for(int i=0; i<100; i++)
shared_print(string("From main: "), i);
t1.join();
return 0;
}
互斥锁std::lock_guard
但是,如果mu.lock()
和mu.unlock()
之间的语句发生了异常,unlock()
语句没有机会执行!导致导致mu
一直处于锁着的状态,其他使用shared_print()
函数的线程就会阻塞。解决这个问题也很简单,使用c++
中常见的RAII
技术,即获取资源即初始化(Resource Acquisition Is Initialization)技术,这是C++
中管理资源的常用方式。简单的说就是在类的构造函数中创建资源,在析构函数中释放资源,因为就算发生了异常,C++
也能保证类的析构函数能够执行。C++
库提供了std::lock_guard
类模板,使用方法如下:
void shared_print(string msg, int id) {
//构造时上锁,析构时释放锁
std::lock_guard<std::mutex> guard(mu);
cout << msg << id << endl;
}
mutex
上锁了,却一直不释放,另一个线程访问该锁保护的资源的时候,就会发生死锁,这种情况下使用lock_guard
可以保证析构的时候能够释放锁,然而,当一个操作需要使用两个互斥元的时候,仅仅使用lock_guard
并不能保证不会发生死锁。mutex
的地址,每次都先锁地址小的if(&_mu < &_mu2){
_mu.lock();
_mu2.lock();
}
else {
_mu2.lock();
_mu.lock();
}
2. 使用层次锁,将互斥锁包装一下,给锁定义一个层次的属性,每次按层次由高到低的顺序上锁
C++
标准库中提供了std::lock()
函数,能够保证将多个互斥锁同时上锁
std::lock(_mu, _mu2);
这两种办法其实都是严格规定上锁顺序,只不过实现方式不同。同时,lock_guard
也需要做修改,因为互斥锁已经被上锁了,那么lock_guard
构造的时候不应该上锁,只是需要在析构的时候释放锁就行了,使用std::adopt_lock
表示无需上锁
std::lock_guard<std::mutex> guard(_mu2, std::adopt_lock);
std::lock_guard<std::mutex> guard2(_mu, std::adopt_lock);
std::unique_lock
互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域。而lock_guard
只能保证在析构的时候执行解锁操作,l
没有提供加锁和解锁的接口。
unique_lock
提供了lock()
和unlock()
接口,能记录现在处于上锁还是没上锁状态,在析构的时候,会根据当前状态来决定是否要进行解锁,可以使用std::defer_lock
设置初始化的时候不进行默认的上锁操作
void shared_print(string msg, int id) {
std::unique_lock<std::mutex> guard(_mu, std::defer_lock);
//do something 1
guard.lock();
// do something protected
guard.unlock(); //临时解锁
//do something 2
guard.lock(); //继续上锁
// do something 3
f << msg << id << endl;
cout << msg << id << endl;
// 结束时析构guard会临时解锁
}
条件变量std::condition_variable
C++11中提供了#include <condition_variable>
头文件,其中的std::condition_variable
可以和std::mutex
结合一起使用
wait()
,可以让线程陷入休眠状态,wait()
函数会先调用互斥锁的unlock()
函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作;
notify_one(),唤醒处于wait
中的其中一个条件变量(可能当时有很多条件变量都处于wait
状态);
notify_all()
,可以同时唤醒所有处于wait
状态的条件变量#include <iostream#include <dequ#include <thread>
#include <mutex>
#include <condition_variable>
std::deque<int> q;
std::mutex mu;
std::condition_variable cond;
//生产者
void function_1() {
int count = 10;
while (count > 0) {
std::unique_lock<std::mutex> locker(mu);//上锁
q.push_front(count);//往队头放数据
locker.unlock();//解锁
cond.notify_one(); // 通知一个等待的线程
std::this_thread::sleep_for(std::chrono::seconds(1));
count--;
}
}
//消费者
void function_2() {
int data = 0;
while ( data != 1) {
std::unique_lock<std::mutex> locker(mu);//上锁
while(q.empty())
cond.wait(locker); //解锁并等待通知
data = q.back();//取队尾数据
q.pop_back();//删除队尾数据
locker.unlock();//解锁
std::cout << data << std::endl;
}
}
int main() {
std::thread t1(function_1);//生产者
std::thread t2(function_2);//消费者
t1.join();
t2.join();
return 0;
}
function_2
中,在判断队列是否为空的时候,使用的是while(q.empty())
,而不是if(q.empty())
,这是因为wait()
从阻塞到返回,不一定就是由于notify_one()
函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()
阻塞。原文链接:https://www.jianshu.com/p/c1dfa1d40f53