0.参考资料
std::lock_guard 引起的思考 : 强烈推荐看,写的非常好。
vins-mono使用的多线程代码解读:对VINS中使用的线程锁都进行了介绍,但是写的感觉不是特别明白
C++11 std::unique_lock与std::lock_guard区别及多线程应用实例 :这个讲解了std::unique_lock 与std::lock_guard 的区别,其实他俩基本功能是一样的。
1.std::lock_guard实现自动加锁
1.1.std::lock_guard实现机理
简单来说,在需要使用线程锁保证多线程数据访问的安全性的时候,如果使用手动加锁,那么经常会出现忘记释放锁而导致线程死锁的现象。
std::lock_guard 是C++的一个标准模板类,见如下自己写的类似的类,其模板参数就是传入的std::mutex 类,然后引用传入的实参需要是一个具体的mutex 对象,这样声明一个std::lock_guard 局部对象后,就可以在其生命周期内利用构造函数和析构函数进行自动加锁和解锁。
namespace myspace {
template<typename T> class my_lock_guard {
public:
my_lock_guard(T& mutex) :mutex_(mutex){
mutex_.lock();
}
~my_lock_guard() {
mutex_.unlock();
}
private:
my_lock_guard(my_lock_guard const&);
my_lock_guard& operator=(my_lock_guard const&);
private:
T& mutex_;
};
};
1.2.工程测试
#include <iostream>
#include <mutex>
#include <thread>
int kData = 0;
std::mutex kMutex;
namespace myspace {
template<typename T> class my_lock_guard {
public:
my_lock_guard(T& mutex) :mutex_(mutex){
mutex_.lock();
}
~my_lock_guard() {
mutex_.unlock();
}
private:
my_lock_guard(my_lock_guard const&);
my_lock_guard& operator=(my_lock_guard const&);
private:
T& mutex_;
};
};
void increment() {
for (int i = 0; i < 10; i++) {
std::cout << std::this_thread::get_id()
<< ":" << kData++ << std::endl;
}
}
int main()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
getchar();
return 0;
}
- CMakeLists.txt:这里需要注意的是由于thread使用了pthread库,所以之类必须手动链接到这个库上,否则报错。
cmake_minimum_required(VERSION 2.8.3)
project(mutex_test)
add_executable(mutex_test main.cpp)
target_link_libraries(mutex_test pthread) # 注意这个地方必须链接到pthread库,因为thread编程用到了这个库,如果不链接会出错
执行结果:
- 不加锁:每次执行结果都不一样,比如其中一次出错的结果如下:
- 加锁:程序正常执行,在一个线程访问数据的时候,另一个线程并不能访问这个数据。
2.std::unique_lock与std::lock_guard区别
他们两个的区别不是很大,都可以实现自动加锁和解锁的功能。主要区别可以记住以下几点:
unique_lock 有手动的unlock() 方法,可以手动解锁,而lock_guard 没有,只能等待对象的生命周期结束后自动解锁,所以相对来说不是很灵活;unique_lock 可以配合环境变量condition_variable 实现多线程之间的同步和通信,而lock_guard 不行。
3.condition_variable条件变量
3.1.讲解例1
参考:C++11(六) 条件变量(condition_variable)
3.1.1.原例
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
std::unique_lock<std::mutex> lk(m);
std::cout << "First go to Worker thread\n";
cv.wait(lk, []{return ready;});
std::cout << "Worker thread is processing data\n";
data += " after processing";
processed = true;
std::cout << "Worker thread signals data processing completed\n";
lk.unlock();
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
}
程序执行结果:
上述函数执行的步骤是:
- 在主线程中新开一个
worker 线程,其实此时worker 线程到底何时运行不是很确定,有可能在主线程std::lock_guard<std::mutex> lk(m); 之前就运行了,也有可能在这之后才运行,但是从上面的执行结果来看是在主线程抢得线程锁执行之后,worker 线程才执行; - 主线程执行完43-48行之后(在此过程中
worker 线程无法获得线程锁,一直阻塞),条件变量ready 变为true ,主线程离开lock_guard 的作用域后释放了线程锁。此时49行手动调用cv.notify_one(); 唤醒worker 线程; worker 线程首先获得线程锁,在wait 函数的地方判断ready 条件变量为真,此时继续持有线程锁而不释放,也就是worker 线程继续跑(在此过程中主线程无法获得线程锁,一直阻塞);worker 线程中将条件变量process 变为true ,最后释放线程锁,并在第34行手动调用cv.notify_one(); 唤醒主线程;- 再次回到主线程,此时主线程获得线程锁,在
wait 函数的地方判断process 条件变量为真,此时继续持有线程锁而不释放,也就是主线程继续跑(在此过程中由于worker 线程中没有死循环,上面已经整个worker 线程已经执行完毕,所以这里不会再执行worker 线程)
3.1.2.修改测试
在原来的main 函数第40行增加一句主函数的休眠语句,为了让worker 先执行,代码和程序执行结果如下:
int main()
{
std::thread worker(worker_thread);
std::this_thread::sleep_for(std::chrono::seconds(2));
data = "Example data";
.........
}
3.2.讲解例2
参考:vins-mono使用的多线程代码解读
3.2.1.原例
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
std::condition_variable cv;
std::mutex m;
long balance = 0;
void addMoney(int money) {
std::lock_guard<std::mutex> lg(m);
balance+=money;
cout<<"Amount added current balance: "<<balance<<endl;
cv.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout<<"HHHH"<<endl;
}
void withdrowMoney(int money) {
std::unique_lock<mutex> ul(m);
cout << "First got to withdrowMoney thread" << endl;
cv.wait(ul,[]{return (balance!=0)?true:false;});
if (balance>=money) {
balance -=money;
cout<<"Amount deducted: "<<money<<endl;
}
else{
cout<<"Amount can't be deducted, current balance is less than "<<money<<endl;
}
cout<<"Current Balance is: "<<balance<<endl;
}
int main()
{
std::thread t1(withdrowMoney,500);
std::this_thread::sleep_for(std::chrono::seconds(2));
std::thread t2(addMoney,500);
t1.join();
t2.join();
return 0;
}
程序执行结果:
上述程序执行步骤:
withdrowMoney 先执行,获得线程锁之后,在wait 函数处判断条件变量balance 不满足条件,因此释放线程锁,该线程阻塞挂起;- 2s后
addMoney 线程执行,获得线程锁后修改balance 的值,然后手动调用cv.notify_one(); 唤起withdrowMoney 线程。但是注意:由于此时lock_guard 还未失效,所以线程锁仍然在addMoney 线程手中,所以即使通知了另一个线程也没用,本线程会继续执行,休眠2s,然后输出"HHHH"; addMoney 线程结束释放线程锁,withdrowMoney 被唤醒获得线程锁之后,在wait 函数处判断条件变量balance 满足条件,继续往下执行,知道执行完整个线程。注意由于addMoney 线程中没有死循环,执行完毕之后就不再执行了
3.2.2.修改测试
在addMoney 线程中修改lock_guard 线程锁的作用范围,在调用cv.notify_one(); 之前就释放线程锁,这样才能成功调用另一个withdrowMoney 线程。修改函数如下:
void addMoney(int money) {
{
std::lock_guard<std::mutex> lg(m);
balance+=money;
cout<<"Amount added current balance: "<<balance<<endl;
}
cv.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout<<"HHHH"<<endl;
}
程序执行结果如下,可见“HHHH”是最后才输出的。
|