互斥锁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
并不能保证不会发生死锁。
1. 可以比较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