C++锁总结

  • 锁:mutex

    • mutex 独占的互斥量,不能递归使用 C++11
    • timed_mutex 有超时功能的独占互斥量,不能递归使用 C++11
    • recursive_mutex 递归互斥量,能递归使用 C++11
    • recursive_timed_mutex 有超时功能的递归互斥量 C++11
    • shared_timed_mutex 具有超时机制的可共享互斥量 C++14
    • shared_mutex 共享的互斥量 C++17

    读写锁(std::shared_mutex)
    读写锁相比互斥锁,读写锁允许更高的并行性,互斥量要么锁住状态要么不加锁,而且一次只有一个线程可以加锁。读写锁也叫做“共享-独占锁”,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。

    • 读写锁可以有三种状态:

      • 读模式加锁状态;
      • 写模式加锁状态;
      • 不加锁状态;

    1)若一个线程已经通过lock或try_lock获取独占锁(写锁),则无其他线程能获取该锁(包括共享的)。尝试获得读锁的线程也会被阻塞。

    2)仅当任何线程均未获取独占性锁时,共享锁(读锁)才能被多个线程获取(通过 lock_shared 、try_lock_shared )。

    3)在一个线程内,同一时刻只能获取一个锁(共享或独占性)。

    • 自旋锁

    是一种busy-waiting的锁。从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道“自旋锁”是比较耗费CPU的。

    // 用户空间用 atomic_flag 实现自旋互斥
    #include <thread>
    #include <vector>
    #include <iostream>
    #include <atomic>
    
    std::atomic_flag lock = ATOMIC_FLAG_INIT;
    
    void f(int n)
    {
        for (int cnt = 0; cnt < 100; ++cnt) {
            while (lock.test_and_set(std::memory_order_acquire))  // 获得锁
                ; // 自旋
            std::cout << "Output from thread " << n << '\n';
            lock.clear(std::memory_order_release);               // 释放锁
        }
    }
    
    int main()
    {
        std::vector<std::thread> v;
        for (int n = 0; n < 10; ++n) {
            v.emplace_back(f, n);
        }
        for (auto& t : v) {
            t.join();
        }
    }
    

条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。

头文件:< condition_variable >
类型:std::condition_variable(只和std::mutex一起工作) 和 std::condition_variable_any(符合类似互斥元的最低标准的任何东西一起工作)。
```
//使用std::condition_variable等待数据
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;

void data_preparation_thread()
{
    while(more_data_to_prepare())
    {
        data_chunk const data=prepare_data();
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(data);
        data_cond.notify_one();
    }
}

void data_processing_thread()
{
    while(true)
    {
        std::unique_lock<std::mutex> lk(mut);   //这里使用unique_lock是为了后面方便解锁
        data_cond.wait(lk,{[]return !data_queue.empty();});
        data_chunk data=data_queue.front();
        data_queue.pop();
        lk.unlock();
        process(data);
        if(is_last_chunk(data))
            break;
    }
}
```
  • 锁,是生活中应用十分广泛的一种工具。锁的本质属性是为事物提供“访问保护”,例如:大门上的锁,是为了保护房子免于不速之客的到访;自行车的锁,是为了保护自行车只有owner才可以使用;保险柜上的锁,是为了保护里面的合同和金钱等重要东西……

  • 在c++等高级编程语言中,锁也是用来提供访问保护的,不过被保护的东西不再是房子、自行车、金钱,而是内存中的各种变量。此外,计算机领域对于“锁”有个响亮的名字——mutex(互斥量),学过操作系统的同学对这个名字肯定很熟悉。

  • Mutex,互斥量,就是互斥访问的量。这种东东只在多线程编程中起作用,在单线程程序中是没有什么用处的。从c++11开始,c++提供了std::mutex类型,对于多线程的加锁操作提供了很好的支持。

    • 下面看一个简单的例子,对于mutex形成一个直观的认识

      #include <iostream>
      #include <thread>
      #include <vector>
      #include <mutex>
      #include <chrono>
      #include <stdexcept>
      
      int counter = 0;
      std::mutex mtx; // 保护counter
      
      void increase(int time) {
          for (int i = 0; i < time; i++) {
              mtx.lock();
              // 当前线程休眠1毫秒
              std::this_thread::sleep_for(std::chrono::milliseconds(1));
              counter++;
              mtx.unlock();
          }
      }
      
      int main(int argc, char** argv) {
          std::thread t1(increase, 10000);
          std::thread t2(increase, 10000);
          t1.join();
          t2.join();
          std::cout << "counter:" << counter << std::endl;
          return 0;
      }
      
    • 总结std::mutex:

      1. 对于std::mutex对象,任意时刻最多允许一个线程对其进行上锁
      2. mtx.lock():调用该函数的线程尝试加锁。如果上锁不成功,即:其它线程已经上锁且未释放,则当前线程block。如果上锁成功,则执行后面的操作,操作完成后要调用mtx.unlock()释放锁,否则会导致死锁的产生
      3. mtx.unlock():释放锁
      4. std::mutex还有一个操作:mtx.try_lock(),字面意思就是:“尝试上锁”,与mtx.lock()的不同点在于:如果上锁不成功,当前线程不阻塞。
  • lock_guard

    初始化:
    explicit lock_guard( mutex_type& m ;  (since C++11)
    lock_guard( mutex_type& m, std::adopt_lock_t t ); 
    lock_guard( const lock_guard& ) = delete;    
    

    虽然std::mutex可以对多线程编程中的共享变量提供保护,但是直接使用std::mutex的情况并不多。因为仅使用std::mutex有时候会发生死锁。回到上边的例子,考虑这样一个情况:假设线程1上锁成功,线程2上锁等待。但是线程1上锁成功后,抛出异常并退出,没有来得及释放锁,导致线程2“永久的等待下去”,此时就发生了死锁。

    • std::lock_guard只有构造函数和析构函数。简单的来说:当调用构造函数时,会自动调用传入的对象的lock()函数,而当调用析构函数时,自动调用unlock()函数(RAII)
      #include <iostream>
      #include <thread>
      #include <vector>
      #include <mutex>
      #include <chrono>
      #include <stdexcept>
    
      int counter = 0;
      std::mutex mtx; // 保护counter
    
      void increase_proxy(int time, int id) {
          for (int i = 0; i < time; i++) {
              // std::lock_guard对象构造时,自动调用mtx.lock()进行上锁
              // std::lock_guard对象析构时,自动调用mtx.unlock()释放锁
              std::lock_guard<std::mutex> lk(mtx);
              // 线程1上锁成功后,抛出异常:未释放锁
              if (id == 1) {
                  throw std::runtime_error("throw excption....");
              }
              // 当前线程休眠1毫秒
              std::this_thread::sleep_for(std::chrono::milliseconds(1));
              counter++;
          }
      }
    
      void increase(int time, int id) {
          try {
              increase_proxy(time, id);
          }
          catch (const std::exception& e){
              std::cout << "id:" << id << ", " << e.what() << std::endl;
          }
      }
    
      int main(int argc, char** argv) {
          std::thread t1(increase, 10000, 1);
          std::thread t2(increase, 10000, 2);
          t1.join();
          t2.join();
          std::cout << "counter:" << counter << std::endl;
          return 0;
      }
    
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值