在多线程操作中,锁用来保证数据的一致性访问,即各个线程有条不紊的使用某些数据,避免同时操作或同时取值导致出现问题。
多线程本来就是为了提高效率和响应速度,但锁的使用又限制了多线程的并行执行,这会降低效率,因此设计合理的锁能保证程序效率。
lock_guard
lock_guard是一个互斥量包装程序,它提供了一种方便的RAII(Resource acquisition is
initialization )风格的机制来在作用域块的持续时间内拥有一个互斥量。
创建lock_guard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时,lock_guard析构并释放互斥量。
它的特点如下:
1、创建即加锁,作用域结束自动析构并解锁,无需手工解锁
2、不能中途解锁,必须等作用域结束才解锁
3、不能复制
#include <thread>
#include <mutex>
#include <iostream>
int g_i = 0;
std::mutex g_i_mutex;
void safe_increment()
{
const std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
// g_i_mutex is automatically released when lock
// goes out of scope
}
int main()
{
std::cout << "main: " << g_i << '\n';
std::thread t1(safe_increment);
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
}
unique_lock
unique_lock是一个通用的互斥量锁定包装器,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。
简单地讲,unique_lock 是 lock_guard 的升级加强版,它具有 lock_guard 的所有功能,同时又具有其他很多方法,使用起来更强灵活方便,能够应对更复杂的锁定需要。unique_lock是个类模板,灵活很多,但效率上差一点,内存占用多一点。工作中一般推荐使用lock_guard;lock_guard取代了mutex的lock()和unlock();
unique_lock特点如下:
1、创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
2、可以随时加锁解锁
3、作用域规则同 lock_grard,析构时自动释放锁
4、不可复制,可移动
5、条件变量需要该类型的锁作为参数(此时必须使用unique_lock)
第二个参数:
std::lock_guard<std::mutex> lk1(my_mutex1, std::adopt_lock);// std::adopt_lock标记作用;
std::unique_lock<std::mutex> lk2(my_mutex2, std::adopt_lock);// std::adopt_lock标记作用;
std::adopt_lock
表示这个互斥量已经被lock了(你必须要把互斥量提前lock了 ,否者会报异常);
std::adopt_lock标记的效果就是假设调用一方已经拥有了互斥量的所有权(已经lock成功了);则通知lock_guard或unique_lock不需要再构造函数中lock这个互斥量了。
用std::adopt_lock的前提是,自己需要先把mutex lock上。
std::try_to_lock
我们会尝试用mutex的lock()去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里;用这个try_to_lock的前提是你自己不能先lock。
std::defer_lock
std::defer_lock的意思就是并没有给mutex加锁:初始化了一个没有加锁的mutex。
用std::defer_lock的前提是,你不能自己先lock,否则会报异常。
条件变量condition variable
说完unique_lock此时不得不再总结一些相关用法——condition variable
在多线程编程中,当多个线程之间需要进行某些同步机制时,如某个线程的执行需要另一个线程完成后才能进行,可以使用条件变量。
c++11提供的 condition_variable 类是一个同步原语,它能够阻塞一个或者多个线程,直到另一线程修改共享变量并通知 condition_variable。
也可以把它理解为信号通知机制,一个线程负责发送信号,其他线程等待该信号的触发。
condition_variable 存在一些问题,如虚假唤醒,这可以通知增加额外的共享变量来避免。
针对增加额外的变量这一点,为什么不在另一线程循环检测这个变量,从而达到相同目的而不需要再使用条件变量?
循环检测时,程序在高速运行,占用过高的cpu,而条件变量的等待是阻塞,休眠状态下cpu使用率为0,省电!
对于运行中的线程,可能会被操作系统调度,切换cpu核心,这样一来,所有的缓存可能失效,而条件变量不会,省时!
对于只需要通知一次的情况,如初始化完成、登录成功等,建议不要使用 condition_variable,使用std::future更好。
对于共享变量的刷新,发出通知的一方流程如下:
1、可以通过 std::lock_guard来获取 std::mutex;
修改共享变量(即使共享变量是原子变量,也需要在互斥对象内进行修改,以保证正确地将修改发布到等待线程)
在 condition_variable 上执行 notify_one/notify_all 通知条件变量(该操作不需要锁)
对于锁的等待一方,流程如下:
1、先获取 std::mutex, 此时只能使用 std::unique_lock
2、执行 wait,wait_for或wait_until(该操作会自动释放锁并阻塞)。
3、接收到条件变量通知、超时或者发生虚假唤醒时,线程被唤醒,并自动获取锁。唤醒的线程负责检查共享变量,如果是虚假唤醒,则应继续等待。
std :: condition_variable仅适用于 std::unique_lock,此限制允许在某些平台上获得最大效率。
std:: condition_variable_any提供可与任何BasicLockable对象一起使用的条件变量,例如std ::
shared_lock。
使用注意事项:
1、notify_one() 只唤醒一个线程,如果有多个线程,具体唤醒哪一个不确定,如果需要唤醒其他所有线程,使用 notify_all()
2、执行 notify_one() 时不需要锁。
3、修改共享变量 ready/processed 时需要锁,共享变量用于避免虚假唤醒。
4、cv.wait 第一个参数必须是 unique_lock,因为它内部会执行 unlock和lock,如果需要设置超时,使用 wait_for/wait_until。
5、需要共享变量来避免虚假唤醒,接收线程需要判断是否为虚假唤醒。如果不使用共享变量,当通知线程在接收线程准备接收之前发送通知,接收线程将要永远阻塞了。
6、共享变量的修改需要在锁内进行。
7、通知线程在发出通知时不需要加锁。
std::condition_variable 提供了两种 wait() 函数。当前线程调用 wait()后将被阻塞(此时当前线程应该获得了锁(mutex),假设获得了锁 lck),直到另外某个线程调用 notify_one/all唤醒了当前线程。
1、在线程被阻塞时,该函数会自动调用 lck.unlock()释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_one/all 唤醒了当前线程),wait() 函数也是自动调用 lck.lock(),使得 lck 的状态和 wait 函数被调用时相同。
2、某些情况下(即设置了 Predicate),只有当 pred 条件为 false 时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred 为 true 时才会被解除阻塞。
上代码:
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.
void do_print_id(int id)
{
std::unique_lock <std::mutex> lck(mtx);
while (!ready) // 如果标志位不为 true, 则等待...
cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
// 线程被唤醒, 继续往下执行打印线程编号id.
std::cout << "thread " << id << '\n';
}
void go()
{
std::unique_lock <std::mutex> lck(mtx);
ready = true; // 设置全局标志位为 true.
cv.notify_all(); // 唤醒所有线程.
}
int main()
{
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(do_print_id, i);
std::cout << "10 threads ready to race...\n";
go(); // go!
for (auto & th:threads)
th.join();
return 0;
}
wait()另一种情况:
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::yield
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
int cargo = 0;
bool shipment_available()
{
return cargo != 0;
}
void consume(int n)
{
for (int i = 0; i < n; ++i) {
std::unique_lock <std::mutex> lck(mtx);
cv.wait(lck, shipment_available);
std::cout << cargo << '\n';
cargo = 0;
}
}
int main()
{
std::thread consumer_thread(consume, 10);
for (int i = 0; i < 10; ++i) {
while (shipment_available())
std::this_thread::yield();
std::unique_lock <std::mutex> lck(mtx);
cargo = i + 1;
cv.notify_one();
}
consumer_thread.join();
return 0;
}
最后说说recursive_mutex
recursive_mutex 类是同步原语,能用于保护共享数据免受从个多线程同时访问。
recursive_mutex 提供排他性递归所有权语义:
调用方线程在从它成功调用 lock 或 try_lock 开始的时期里占有 recursive_mutex 。此时期间,调用方线程可以多次锁定/解锁互斥元。结束的时候lock与unlock次数匹配正确就行。
线程占有 recursive_mutex 时,若其他所有线程试图要求 recursive_mutex 的所有权,则它们将阻塞(对于调用 lock )或收到 false 返回值(对于调用 try_lock )。
可锁定 recursive_mutex 次数的最大值是未指定的,但抵达该数后,对 lock 的调用将抛出 std::system_error 而对 try_lock 的调用将返回 false 。
若 recursive_mutex 在仍为某线程占有时被销毁,则程序行为未定义。 recursive_mutex 类满足互斥体 (Mutex) 和标准布局类型 (StandardLayoutType) 的所有要求。
recursive_mutex与std::lock_guard一起使用有时也能达到某些场合的效果:
#include <iostream>
#include <thread>
#include <mutex>
class X {
std::recursive_mutex m;
std::string shared;
public:
void fun1() {
std::lock_guard<std::recursive_mutex> lk(m);
shared = "fun1";
std::cout << "in fun1, shared variable is now " << shared << '\n';
}
void fun2() {
std::lock_guard<std::recursive_mutex> lk(m);
shared = "fun2";
std::cout << "in fun2, shared variable is now " << shared << '\n';
fun1(); // ① 递归锁在此处变得有用
std::cout << "back in fun2, shared variable is " << shared << '\n';
};
};
int main()
{
X x;
std::thread t1(&X::fun1, &x);
std::thread t2(&X::fun2, &x);
t1.join();
t2.join();
}
共勉。
参考文献:
https://blog.csdn.net/weixin_40179091/article/details/108650433
https://blog.csdn.net/weixin_34014277/article/details/86403169
https://blog.csdn.net/guotianqing/article/details/104002449