互斥

互斥量的锁定

https://cppreference.cn/w/cpp/header/mutex
互斥量 std::mutex 不仅实现了互斥量的实例化,还提供了像 lock() 之类的成员函数来实现手动加锁和解锁。
但是实践中不推荐调用这些成员函数,而应该优先使用RAII风格的 std::lock_guardstd::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_flagstd::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 来一次获得多个锁;不能一次性获取,那么用固定的顺序获取;
posted @ 2025-09-05 15:12  名字好难想zzz  阅读(14)  评论(0)    收藏  举报