第2章 线程同步精要

2.1 互斥器 (mutex)

互斥器保护了临界区,任何时刻最多只能有一个线程在mutex划出的临界区内活动

推荐使用原则:

  • 用RAII手法封装mutex的创建、销毁、加锁、解锁四个操作
  • 只是用非递归的mutex (不可重入的mutex)
  • 不手动调用lock()和unlock()函数,交给栈上的Guard对象的构造和析构负责
  • 使用Guard对象时考虑调用栈上持有的锁,防止加锁顺序不同导致死锁

非递归的mutex

mutex分为递归和非递归,也称为可重入和非可重入。区别在于:同一个线程可以重复对recursive mutex加锁,不能重复对non-recursive mutex加锁。

在同一线程中对non-recursive mutex重复加锁会立刻导致死锁,可以帮助在编码阶段发现问题。

recursive mutex可能会隐藏一些问题,当你以为拿到一个锁就能修改对象,没想到外层代码已经拿到了锁,增在修改或读取同一个对象

MutexLock mutex;
std::vector<Foo> foos;void post(const Foo& f) { // 加锁修改对象MutexLockGuard lock(mutex);foos.push_back(f);
}void traverse() { // 加锁访问对象MutexLockGuard lock(mutex); for (std::vector<Foo>::const_iterator it = foos.begin();it != foos.end(); ++it) {it->doit();}
}

如果Foo::doit()间接调用post,Mutex为非递归就会发生死锁,Mutex为递归可能会导致vector的迭代器失效,程序偶尔crash

解决上面问题有两种做法:

  • 把修改推迟,记录循环中试图添加修改的元素,等到循环结束再调用post
  • 如果一个函数可能在加锁的情况被调用,也可能在为假锁的情况被调用,可以把函数拆成两部分
    • 跟原函数同名,函数加锁,调用第二个函数
    • 函数名加后缀WithLockHold,不加锁,把原来的函数体搬过来
void post(const Foo& f) {MutexLockGuard lock(mutex);postWithLockHold(f); // 编译器自动内联
}
void postWithLockHold(const Foo& f) {foos.push_back(f);
}

如上也会造成两个问题:

  1. 误用加锁版本,死锁
  2. 无用不加锁版本,数据损坏

对于(1)可以通过调用栈进行排错,对于(2)可以在调用的时候判断锁是否时调用线程加的 (isLockedByThisThread)

2.2 条件变量

  • 条件变量和mutex一起使用, 布尔表达式受mutex保护

  • 将布尔条件判断和wait放到while循环中

    muduo::MutexLock mutex;
    muduo::Condition cond(mutex);
    std::deque<int> queue;int dequeue() { // 出队MutexLockGuard lock(mutex);while (queue.empty()) { // 用循环先判断在waitcond.wait();  // 原子操作,unlock mutex进入等待,不与enqueue竞争锁// wait()执行完重新加锁}
    }void enqueue(int x) {MutexLockGuard lock(mutex);queue.push_back(x);cond.notify();
    }
    

    上面代码必须用while循环等待条件变量, 不能用if语句, 可能存在spurious wakeup

  • broadcast通常用于表明状态变化, signal通常用于表示资源可用

  • 条件变量是底层的同步原语,通常用来实现高层的同步措施

    • BlockingQueue

    • DountDownLatch (倒计时)

      • 主线程发起多个子线程,等待子线程各自完成一定任务后, 主线程继续执行, 通常用于主线程等待多个子线程完成初始化
      • 主线程发起多个子线程, 子线程等待主线程, 主线程完成一定任务后通知所有的子线程开始执行,通常用于多个子线程等待主线程发出“起跑”命令
      class CountDownLatch : boost::noncopyable{
      public:explicit CountDownLatch(int count); // 倒数几次void wait();      // 等待计数器变为0void countDown();    // 计数器减1
      private:muteable MutexLock mutex_;Condition condition_;int count_;
      };void CountDownLatch:: wait() {MutexLockGuard lock(mutex_);while (count_ > 0) condition_.wait();
      }void Count DownLatch:: countDown() {MutexLockGuard lock(mutex_);--count_;if (count == 0) condition_.notifyAll();
      }
      

2.3 不要用读写锁和信号量

对写锁

  • 典型错误,在持有read lock时候修改共享数据。不小心在read lock保护的函数中调用了会修改状态的函数。

  • read lock加锁的开销不比mutex lock小,每次要更新reader的个数

  • reader lock的可重入可能造成死锁

    1. 线程1加读锁,进行读操作

    2. 线程2加写锁, 等待线程1,阻塞后面的读操作

    3. 线程1内部间接调用对操作,因为reader lock的可重入,线程1的读操作阻塞

    4. 发生死锁

信号量

作者不建议用互斥量

对于哲学家就餐问题,在教科书的解决方案是平权,每个哲学家有自己的线程,自己去拿筷子。作者认为集权的方式,用一个线程专门负责餐具的分配,让其他哲学家拿着号等在食堂门口(condition variable),这样不损失多少效率同时简化程序

2.4 小结

  • 线程同步尽量用高层同步设施(线程池、队列、倒计时)
  • 让一个正确的程序变快,远比“让一个快的程序变正确”容易的多
  • 真正影响性能的不是锁,而是锁争用(lock contention)
  • sleep不是同步原语,尽量少使用,可以采用唤醒、轮询、timer的方式
  • 使用shared_ptr,读操作增加引用计数,写的时候用shared_ptr::unique()判断是否有其他用户在读。如果没有用户读直接修改,如果有可以拷贝一份,在副本上修改。使用shared_ptr::swap()更新指针

muduo学习笔记 - 第2章 线程同步精要相关推荐

  1. 《Windows via C/C++》学习笔记 —— 内核对象的“线程同步”之“信号量”

    "信号量内核对象"用于对资源进行计数. 在信号量内核对象内部,和其他内核对象一样,有一个使用计数,该使用计数表示信号量内核对象被打开的次数. 信号量内核对象中还有两个比较重要的数据 ...

  2. muduo学习笔记 - 第五章 高效的多线程日志

    第五章 高效的多线程日志 日志有两种意思: 诊断日志 交易日志 本章讲的是前一种日志,文本的供人阅读的日志,通常用于故障诊断和追踪,也可用于性能分析. 日志通常要记录: 收到的每条消息的id(关键字段 ...

  3. muduo学习笔记 - 第3章 多线程服务器的适合场合与常用编程模型

    第3章 多线程服务器的适合场合与常用编程模型 3.1 基本概念 同步和异步 针对程序和内核的交互 同步:用户进程触发IO操作,等待或轮询的查看IO是否就绪 异步:用户进程触发IO操作,继续做自己的事情 ...

  4. muduo学习笔记 - 第4章 C++多线程系统编程精要

    第4章 C++多线程系统编程精要 Pthreads只保证统一进程之内,同一时刻的各个线程的id不同,不能保证同一进程先后多个进程具有不同的id,更不要说一台机器上多个进程之间的id唯一性,pthrea ...

  5. muduo学习笔记 - 第1章 C++多线程系统编程

    第1章 C++多线程系统编程 1.1 智能指针 C++中动态内存管理是用new和delete完成. 动态内存管理经常出现两种问题: 忘记释放内存造成内存泄露 还有指针引用的内存的情况下释放内存,造成引 ...

  6. 【学习笔记】第二章——线程与多线程模型

    文章目录 一. 线程 为什么要引入线程: 引入进程后的变化 线程的属性 线程的实现 二. 多线程模型 1. 多对一模型 2. 一对一模型 3. 多对多模型 三. 总结 一. 线程 为什么要引入线程: ...

  7. Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25

    Effective Java(第三版) 学习笔记 - 第四章 类和接口 Rule20~Rule25 目录 Rule20 接口优于抽象类 Rule21 为后代设计接口 Rule22 接口只用于定义类型 ...

  8. C# 学习笔记(9)线程

    C# 学习笔记(9)线程 本文参考博客 C#多线程 https://www.cnblogs.com/dotnet261010/p/6159984.html C# 线程与进程 https://www.c ...

  9. Windows事件等待学习笔记(二)—— 线程等待与唤醒

    Windows事件等待学习笔记(二)-- 线程等待与唤醒 要点回顾 等待与唤醒机制 可等待对象 可等待对象的差异 线程与等待对象 一个线程等待一个对象 实验 第一步:编译并运行以下代码 第二步:在Wi ...

最新文章

  1. 疫情之下,武汉女生在家中答辩,获得国外博士学位!
  2. 使用Innobackupex快速搭建(修复)MySQL主从架构
  3. Asp.Net Core 使用Quartz基于界面画接口管理做定时任务
  4. [SharePoint 2010] Client Object Model 跨时区查询list item的方法
  5. JavaScript玩转机器学习:平台和环境
  6. 一文读懂Java多线程原理
  7. ecshop上传图片2
  8. 在项目中配置PageHelper插件时遇到类型转换异常
  9. 通过增强的 Windows Forms 支持为 .NET 应用程序精心制作华丽的 UI
  10. JSP基于JDBC操作MSSQL2008数据库
  11. 2019-12-11 转载TCP/IP编程常用C语言头文件
  12. php面向对象有哪三种访问控制模式,第三节 访问控制的实现
  13. 关于c++库文件的一点使用体会
  14. 基于51单片机的医院银行排队叫号系统proteus仿真程序原理图设计
  15. 配置文件填写内网地址和127.0.0.1的区别
  16. Linux重定向console口控制台,Linux重定向console口控制台(Fedora)
  17. 苹果手机的计算机怎么设置快捷键大全,教程方法;苹果电脑快捷键大全最常用的都在这里了电脑技巧-琪琪词资源网...
  18. IP协议(网际协议)
  19. 人民币对美元汇率中间价报6.7025元 上调318个基点
  20. java手风琴代码_JavaScript实现手风琴效果

热门文章

  1. E. 存储过程(procedure)
  2. 引用第三方高德地图接口---使用js脚本进行开发地图定位的步骤
  3. css04使用外部样式
  4. 第六十节,文本元素标签
  5. Ajax中什么时候用同步,什么时候用异步?
  6. 企业c语言的编程风格,c语言优秀编程风格.docx
  7. [系统安全] 四.OllyDbg动态分析工具基础用法及Crakeme逆向破解
  8. 【网络通信与信息安全】之深入分析Token、session和cookie的使用场景和区别
  9. 404. Sum of Left Leaves 左叶子之和
  10. 51. N-Queens N 皇后