C++有什么锁,unique_lock和lock_guard如何实现的

互斥锁

条件锁

std::condition_variable 需要结合互斥锁使用。

  1. //消费者
  2. std::unique_lock<std::mutex> lock(mtx);
  3. cond_.wait(lock,[]()->bool{
  4. return !task_array.empty();})
  5. //生产者
  6. std::unique_lock<std::mutex> lock(mtx);
  7. task_array.push_back(something);
  8. cond_.notify_all();

自旋锁

可通过std::atomic_flag(无锁实现)实现C++的自旋锁机制

atomic原理:原子操作是由底层硬件支持的一种特性 通过简单的自加操作,看其汇编代码,可看到汇编代码中存在一个带lock前缀的命令。该命令保证了程序在load-add-store三个步骤的不可分割。

  • lock的原理是什么?

cpu执行任务时,内存先将数据压入L1和L2 cache中,然后cpu从cache读取数据进行操作。 在早期处理器中,lock命令是锁总线的。现代处理器中,lock通过锁cache来实现多进程或多线程之间的原子性。

如何解决自旋锁长期占用CPU问题

可以结合互斥锁,设置一个尝试次数阈值,在达到该阈值后,转用互斥锁(此互斥锁须设置一个超时时间,wait_for,超时时间随着尝试失败的轮数不断增加)。

  1. class spin_lock
  2. {
  3. public:
  4. spin_lock()
  5. {
  6. while (flag_.test_and_set(std::memory_order_acquire));
  7. }
  8. ~spin_lock()
  9. {
  10. flag_.clear(std::memory_order_release);
  11. }
  12. spin_lock(const spin_lock&) = delete;
  13. spin_lock& operator=(const spin_lock&) = delete;
  14. private:
  15. static std::atomic_flag flag_;
  16. };
  17. std::atomic_flag spin_lock::flag_ = ATOMIC_FLAG_INIT;

递归锁

getpid()得到的是进程的pid,在内核中,每个线程都有自己的PID,要得到线程的PID,必须用syscall(SYS_gettid); pthread_self函数获取的是线程ID,线程ID在某进程中是唯一的,在不同的进程中创建的线程可能出现ID值相同的情况。

递归锁,即可重入锁。线程可重复加锁而不会造成死锁。

  • 线程占用recursive_mutex时,其所有权在线程调用unlock匹配次数结束时结束
  • 其他线程对已被占用的递归锁尝试加锁时,会阻塞或收到false(调用try_lock时)

    1. #pragma once
    2. #include <mutex>
    3. #include <thread>
    4. class recur_lock
    5. {
    6. recur_lock():
    7. cnt_of_lock(0){}
    8. void lock()
    9. {
    10. if(cnt_of_lock==0)
    11. {
    12. mtx_.lock();
    13. owner_ = std::this_thread::get_id();
    14. }else if(owner_==std::this_thread::get_id())
    15. {
    16. ++cnt_of_lock;
    17. }
    18. }
    19. void unlock()
    20. {
    21. if(cnt_of_lock>0)
    22. {
    23. --cnt_of_lock;
    24. }
    25. if(cnt_of_lock==0)
    26. {
    27. mtx_.unlock();
    28. }
    29. }
    30. private:
    31. ::size_t cnt_of_lock;
    32. std::mutex mtx_;
    33. std::thread::id owner_;
    34. };

内联函数相关,内联函数的优缺点

函数调用必须将程序的执行顺序转移到函数所存放的内存的某个位置,函数调用完成后,又需要返回执行该函数的下一条命令。这就要求函数调用必须要在执行前保存现场,并在执行后将现场恢复,这必将在时间和空间上有一定的损耗。

  • 函数调用需要保存调用前的现场,执行完需要恢复现场

    宏在调用的地方,仅仅是将代码替换展开。不会出现函数调用压栈出栈的时间和空间的损耗。

  • 缺点:

    • 容易出现边界性问题,二义性问题
    • 不能访问类的privateprotected对象

      内联函数

      内联函数不同于“宏由预处理器处理【不会进行类型安全检查,自动类型转换】”,其由编译器处理,如编译器未发现内联函数体存在语法错误,则将其【声明与定义】一同放入符号表中。
      在调用内联函数时,和调用普通函数一样,编译器会进行类型安全检查和自动类型转换等操作,检查调用的合法性,若无问题,将内联函数体展开并进行优化。假设内联函数为成员函数,也会自动处理对象指针。

      优点

  • 内联函数会被放入符号表中,编译阶段会进行替换(像宏展开一样),效率高

  • 减少因函数调用引起的开销,主要时参数压栈,帧栈创建和回收,寄存器的保存与恢复
  • 编译器可将内联部分可和调用内联的上下文一起优化,进行更深层次的优化。
  • 内联函数可作为成员函数,访问私有成员和protected成员。

    在类内定义的函数默认为内联函数,但在类外定义的成员函数,声明和定义需要在同一文件 因为内联函数就地展开,故需要将声明和定义放于同一个文件中。因为编译器进行就地置换时,必须知道该内联函数的函数体代码,而不能通过参考其他编译单元来获得这一信息

缺点

  • 频繁调用内联函数会造成“代码膨胀”问题
  • 内联函数中若存在循环体,执行函数体的开销会大于调用成本

    由于内联函数是就地置换,编译器不会对函数体过多优化。所以可能存在调用成本低于函数体执行成本的问题。

内联函数
处理阶段 预处理器 编译器
调用阶段 代码替换 类型安全检查,自动类型转换后,将函数体展开。并联系上下文进行优化。
访问private,protected NO YES!