目录
- 互斥量的锁定
- 不仅仅是锁
- 谨慎的设置接口
- 初始化的保护
- 死锁避免
互斥量的锁定
https://cppreference.cn/w/cpp/header/mutex
互斥量 std::mutex 不仅实现了互斥量的实例化,还提供了像 lock() 之类的成员函数来实现手动加锁和解锁。
但是实践中不推荐调用这些成员函数,而应该优先使用RAII风格的 std::lock_guard 和 std::unique_lock。
这是因为如果线程在加锁后异常退出,锁资源不会被释放,有可能导致死锁。
#include <mutex>
#include <thread>
#include <iostream>
using namespace std;mutex mtx;
void dangerousFunction(int id) {mtx.lock();//线程2永远无法运行std::cout << "Thread " << id << " is running." << std::endl;if (id == 1) {throw std::runtime_error("Thread 1 encountered an error!");}mtx.unlock();
}
int main() {try {std::thread t1(dangerousFunction, 1);std::thread t2(dangerousFunction, 2);t1.join();t2.join();}catch (const std::exception& e) {std::cerr << "Exception caught: " << e.what() << std::endl;}return 0;
}
而 std::lock_guard 基于RAII实现,
- 能够自动加锁,创建 std::lock_guard 对象时会自动加锁,如果互斥量已经被其他线程锁定,那么当前线程将会被阻塞,直到互斥量可用为止;
- 自动解锁,在作用域结束时会自动释放互斥量;
- 不支持手动加锁、解锁,不支持条件变量;
- 禁止复制和移动;
std::unique_guard 相比 std::lock_guard 更加灵活,除了自动加锁、解锁外,它还支持:
- 提供lock()和unlock(), 支持手动加锁、解锁:
- 禁止复制,但支持移动语义,允许在不同作用域和函数间转移互斥量的所有权。;
- 在构造对象时支持延迟加锁,支持尝试锁定;
std::unique_guard 虽然具有更强的性能但是也有更高的开销。
不仅仅是锁
谨慎的设置接口
在大多数情况下,互斥量通常会与保护的数据放在同一个类中,对其的操作声明为成员函数,这样就实现了封装和保护。而当其中一个成员函数返回的是保护数据的指针或引用时,就会破坏对数据的保护。具有访问能力的指针或引用可以访问(并可能修改)被保护的数据,而不会被锁限制。如果允许用户传入函数,那么需要确保用户不会保留指针或引用,但这很难保证,所以不推荐。
初始化的保护
假设有一个共享资源,只需要在初始化的时候进行保护,而后续的操作可能是只读的。而加载它的代价很大,常见的做法是在真正要使用这块数据时,才构造它(延迟初始化)。
std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;void foo()
{std::unique_lock<std::mutex> lk(resource_mutex); // 所有线程在此序列化 if(!resource_ptr){resource_ptr.reset(new some_resource); // 只有初始化过程需要保护 }lk.unlock();resource_ptr->do_something();
}
如上的实现方法会降低程序执行的效率,因为即使共享数据已经初始化好,每次调用 foo() 函数,还是会锁定 resource_mutex,所有调用这个函数的线程,都会被不必要的串行化起来。
c++提供了 std::once_flag 和 std::call_once。
std::shared_ptr<some_resource> resource_ptr;std::once_flag resource_flag; void init_resource(){resource_ptr.reset(new some_resource);}void foo(){std::call_once(resource_flag,init_resource); // 可以完整的进行一次初始化resource_ptr->do_something();}
死锁避免
- 避免在持有锁时调用用户提供的代码;
- 当要获取多个锁时:使用 std::lock 来一次获得多个锁;不能一次性获取,那么用固定的顺序获取;