Chapter 4:同步
本章主要介绍《C++并发编程实战》的第四章内容。
condition variables && future
很重要的一点,多线程开发使用的库函数,比如stl,boost,qt,哪些是线程安全或者可重入的吗?如果不明确就使用,问题就大了⊙▽⊙
扩展阅读:
并发编程系列之一:锁的意义
使用condition variables来等待事件,使用future来等待一次性事件
使用Condition variables来等待由另一个线程触发事件。C++提供了std::condition_variable
和std::condition_variable_any
,二者结合互斥元可以提供恰当的同步。前者只能和mutex一起工作,而后者_any可以与符合成为类似互斥元的最低标准的任何东西一起工作。
如下面的例子,通过condition_variable的语义,可以执行等待,除非满足条件否则线程阻塞。(在这里如果不使用条件等待,那么可以采用:循环地check是否满足条件;或者sleep一段时间再check,不满足再睡。但,这两种方式都会占用比较多的运行时间!所以不如使用condition.notify来得便利)
同时,这个例子也是很简洁的使用队列共享传输数据的场景之一,可以学习如何应用。
Building a thread-safe queue with condition variables
要构造一个线程安全的queue,那么一定需要保护共享变量的线程安全性。通过加锁可以达到这个要求,在这里还运用到了条件变量,每push一个元素,就会发出一个notify。而对于wait_and_pop()
它在执行时候会在cond.wait(guard, [this]{ return !data.empty();})
处阻塞,如果条件不满足则释放锁并继续等待,等待cond.notify_one()
来唤醒。
一次性事件之future
future的语义是一次性事件,一旦事件发生,future变成就绪态ready,然后无法复位。future是可移动的,而shared_future是可拷贝的,这表明一次只有一个实例指向特定的future状态。因此如果从多个线程访问单个future对象而不进行额外的同步,会出现data race和未定义行为。
同时也存在基于时间限制的等待,不过要注意,在计算机中存在多种时间,有的时间种类是非匀速的,在使用时候需要辨别。
可以使用std::async、packaged_task<>、或者promise<T>与future相结合来构建异步事件。
使用async可以启动一个异步任务,它返回一个future对象,当你需要任务的返回值的时候只要调用future的get(),线程会阻塞直到future就绪,然后返回该值,如下代码:
async
同步的其他方式
除去使用条件、事件,还可以使用其他方式来进行同步。这里涉及到不同的多线程并发模型,在翻阅了《七周七并发模型》后才理解4.4后续章节所描述的函数式编程(Functional Programming)、通信式编程(Communicating Sequential Process类似状态转换机或称actor model
角色模型)其实是实现并发的不同方式。
函数式编程指的是函数调用的结果仅单纯依赖该函数的参数而不依赖任何外部状态,纯函数不修改任何外部状态,函数的影响完全局限在返回值上。这样也就不存在修改共享数据
。而类似Haskell这样的编程语言,其所有函数在默认情况下都是纯函数
。对于C++来说,由于它是多范式语言,因此其实也可以通过一些手段达到FP风格的编写。
另外一种方式通信顺序处理
的思想也很简单,没有数据共享,每个线程可以独立推理得到,只需要基于它对接收到的消息
进行反应。这样其实就是抽象成为了一个状态机:当它收到消息,就会依据初始状态进行操作,并以某种方式更新其状态,并能向其他线程发送消息(在实际实现中可能消息队列是共享的,但线程间不存在共享数据)。不过,这非常考验设计人员在编码前对任务的正确划分,同时,在当前一些开发中其实也可以发现有的就是这样一种思路。
在演示Actor model时候,示例代码的函数指针用得有趣。
从这两方式都可以看出如何正确编写并发程序的思路,那就是:控制数据共享!
在线程间划分工作的技术
不同的任务划分方式对数据共享,编码逻辑清晰都有影响需要仔细考虑。
-
从数据划分
在开始前划分数据;
递归地划分数据 -
从任务划分
按任务类型划分:分工,比如刷墙,水管,装电
按任务序列划分:比如买菜,洗菜,做菜
疑问
1.在代码最后,有一个检查是否为最后一个数据,如果是则退出,这一个地方是否也可以使用触发的方式来替代?
线程被关闭,在运行时候是否有类似方案?因为在Qt里如果用了信号槽,强行关闭线程是可能会引发问题的。
通过后续的学习发现其实中断线程的一种方法就是通过条件变量测试,比如主线程Button触发终止线程,导致修改线程的共享变量,在线程执行过程中会多次check该变量是否表示退出,如果表示退出就break循环或其他方式终止后续操作。不过要注意保护共享变量,同时这里也可能涉及到编译器优化,比如while(bState)
,编译器可能
会直接把bState的值存储到寄存器中,因此即使该值发生了变更,执行线程并不会识别到!于是就有人提出了使用violate
来修饰变量使得强制编译器每次都强制从内存中读取该变量。但有文章C/C++ Volatile关键词深度剖析.何登成又说其实这并没有太多效果。
中断线程是一个充满了风险的操作,在Qt中通常quit、exit和terminate,其实quit和exit仅仅是退出线程的事件循环(event-loop),如果该线程没有事件循环,那么其实它什么都不做,所以调用了quit、exit其实很多情况,线程的流程还是会继续走下去。而只要调用terminate,线程会立马终止。但终止一个运行中的线程会导致资源未清理、事件未完成,严重的会导致程序Crush!中断需谨慎。
PS:最近翻了多本介绍多线程开发的书籍,从《Thinking in java》、《C++面向对象多线程编程》、《CLR Via C#》、《深入理解计算机系统》到《C++并发编程实战》发现基本知识其实都一致且不多,短小精炼,但要能真正在Coding时候使用多线程技术,并不是一件容易的事,事事可期,不可预料的感觉。
PS:乱入一个Jay的视频~~
周杰伦的逆袭