8、介绍一下几种典型的锁【中高频】

1.普通互斥量(也叫 互斥锁)(std:: mutex)

在这里插入图片描述

  • 注意,线程函数调用 lock() 时,可能会发生以下三种情况:

    • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
    • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
    • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)
  • 线程函数调用 try_lock() 时,可能会发生以下三种情况:

    • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量

    • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉

    • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

2.递归互斥量(std:: recursive_mutex)
  • 递归互斥锁 允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权。释放互斥量时需要调用相同次数的 unlock()。除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同

  • 这对于递归函数 可能需要在同一线程中多次获取锁的情况很有用:

  #include <iostream>
  #include <mutex>
  #include <thread>
  
  std::recursive_mutex myRecursiveMutex;
  
  void recursiveAccess(int depth) {
      std::unique_lock<std::recursive_mutex> lock(myRecursiveMutex);
      if (depth > 0) {
          recursiveAccess(depth - 1);
      }
      // 访问共享资源的代码
      std::cout << "Accessing shared resource at depth " << depth << "...\n";
  }
  
  int main() {
      std::thread t1(recursiveAccess, 3);
  
      t1.join();
  
      return 0;
  }
3.定时互斥量(std::timed_mutex)
  • 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until() :

    • try_lock_for():函数参数表示一个时间范围,在这一段时间范围之内线程如果没有获得锁 则保持阻塞;如果在此期间其他线程释放了锁,则该线程可获得该互斥锁;如果超时(指定时间范围内没有获得锁),则函数调用返回false。
timed_mutex myMutex;
chrono::milliseconds timeout(100);  //100毫秒
if (myMutex.try_lock_for(timeout))
{
	//在100毫秒内获取了锁
	//业务代码
	myMutex.unlock();  //释放锁
}
else
{
	//在100毫秒内没有获取锁
	//业务代码
}
  • try_lock_until():函数参数表示一个时刻,在这一时刻之前线程如果没有获得锁则保持阻塞;如果在此时刻前其他线程释放了锁,则该线程可获得该互斥锁;如果超过指定时刻没有获得锁,则函数调用返回false。
4.定时递归互斥量(std::recursive_timed_mutex)
  • 允许同一线程多次获取锁,并提供了超时功能。与std::timed_mutex一样,std::recursive_timed_mutex也提供了try_lock_for()和try_lock_until()方法
5.读写锁(std::shared_mutex) C++17开始才有
  • 读写锁主要用于区分对共享资源的 读操作 和 写操作。它有两种获取模式:共享模式(读模式)和独占模式(写模式)。

  • 读写锁机制:多个线程可以共同读取一共共享资源,也就是共享读;但只有一个线程可以写入也就是修改这个共享资源,其他线程都会阻塞,既不能写,也不能读。总之,当一个线程获取 读锁 的时候,其他线程可以获取读锁,但不能获取写锁;当一个线程获取 写锁 的时候,其他线程既不能获取读锁,也不能获取写锁。

  • 有 大量读操作 和 少量写操作 时,可以提高程序的并发性能。例如,在一个缓存系统中,多个线程可能经常读取缓存中的数据,只有在缓存数据需要更新时才会进行写操作,使用读写锁可以很好地处理这种情况。

//shared_mutex 支持共享锁和独占锁,
//std::shared_lock<std::shared_mutex> lock(sharedMutex); //共享锁,即读锁
//std::unique_lock<std::shared_mutex> lock(sharedMutex); //独占锁,即写锁
#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>

std::shared_mutex smtx;
int shared_data = 0;

void read_data()
{
    std::shared_lock<std::shared_mutex> lock(smtx);
    std::cout << "Read data: " << shared_data << std::endl;
}

void write_data(int new_value)
{
    std::unique_lock<std::shared_mutex> lock(smtx);
    shared_data = new_value;
    std::cout << "Wrote data: " << shared_data << std::endl;
}

int main()
{
    std::vector<std::thread> read_threads;
    for (int i = 0; i < 5; i++)
    {
        read_threads.push_back(std::thread(read_data));
    }
    std::thread write_thread(write_data, 10);
    for (auto& t : read_threads)
    {
        t.join();
    }
    write_thread.join();
    return 0;
}
6.自旋锁(通常用 std::atomic_flag 实现) C++11
  • 与传统的互斥锁(Mutex)不同,当线程没有获得锁时 并不会阻塞,而是在一个循环中不断检查锁的状态,直到成功获取锁。这种行为被称为“自旋”。

  • 优缺点:

    • 自旋锁获取锁时 不需要进行上下文切换,节省开销。
    • 但是,如果循环等待的时间过长,线程会一直占用 CPU 资源(进行检查),从而导致 CPU 资源的浪费。所以自旋锁并不适合长时间等待

    img

  //std::atomic_flag 可以实现无锁编程,它只能进行两种操作:获取锁(test_and_set())和释放锁(clear())。
  //test_and_set(): 设置标志为true,但返回之前的值,即获取锁
  //clear(): 将标志设置为false,即释放锁
  /*
  bool test_and_set(bool *target) 
  {
  	bool rv = *target;
  	// 通过地址寻址,真实地修改target的值
  	*target = true;
  
  	// 返回的是传入时target的值
  	return rv;
  }
  */
  #include <atomic>
  #include <thread>
  #include <iostream>
   
  class SpinLock {
  private:
      std::atomic_flag flag = ATOMIC_FLAG_INIT; // 初始化为清除状态即false(无人获取锁)
   
  public:
      void lock() {
          //只有memory_order_acquire为false,表示还没有线程获取锁,所以当前线程可以跳出循环,也就是获取锁。
          //当memory_order_acquire为true时,就相当于有线程已经获取锁了,当前线程要阻塞(即自旋)
          while (flag.test_and_set(std::memory_order_acquire)) {
              // 忙等待,直到获得锁
          }
      }
   
      void unlock() {
          flag.clear(std::memory_order_release);//释放锁
      }
  };
   
  SpinLock spinlock;
  int counter = 0;
   
  void increment() {
      for (int i = 0; i < 100000; ++i) {
          spinlock.lock();
          counter++;
          spinlock.unlock();
      }
  }
   
  int main() {
      std::thread t1(increment);
      std::thread t2(increment);
   
      t1.join();
      t2.join();
   
      std::cout << "Counter: " << counter << std::endl;
   
      return 0;
  }
7.悲观锁与乐观锁

悲观锁

前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。

  • 悲观锁做事比较悲观,也就是未雨绸缪。它认为 多线程同时访问共享资源的概率 比较高,很容易出现冲突,所以要先上锁,才能 访问共享资源

  • 那相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。

乐观锁

乐观锁做事比较乐观,它假定 冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突如果 没有其他线程 在修改资源,则修改完成;如果发现有其他线程同时修改这个资源,就放弃本次操作。

  • 可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。比如在线文档 就是 乐观锁

  • 乐观锁虽然全程并没有加锁,但是一旦发生冲突,重试的成本非常高。所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值