整本书的2-4章,就是介绍C++11的标准库关于多线程的基本使用。
第2章介绍thread,用于创建多线程。
第3章介绍mutex,用于共享数据。
第4章介绍condition_variable和future,用于同步线程。
本帖总结第3章内容。主要的类是互斥量,std::mutex还有上锁的模板类std::lock_guard<std::mutex>
本章的主要概念为:竞争条件(race condition)、死锁(deadlock)
注:11/03 补充unique_lock和lock_guard的比较
1.竞争条件
(1)概念
在多线程并发中,如果一个数据能够被多个线程访问到,那么这个数据就是共享数据。
竞争条件就是结果取决于两个及以上线程操作执行的相对顺序的一切事物。
通常出现问题的竞争条件是会破坏数据结构的不变量(即数据结构固有的结构性质)。
(2)解决办法
①互斥元(mutex)加锁-————加了锁的数据,就可以保证同时只能有一个进程获得加锁范围内的数据的操作权限。
②无锁编程(lock-free programming)
③事务(transaction)
本书最主要的就是讲解mutex的使用
(3)mutex的使用
头文件<mutex>
创建互斥元对象:std::mutex
成员函数:lock()和unlock()
类模板,实现RAII加锁:std::lock_guard
注:实际上就可以把互斥元mutex当做是一把锁,而lock(),unlock(),lock_guard就是对应着加锁解锁的动作。
只是lock_guard会销毁时自动解锁。
(4)mutex的使用注意(如何使得自己能够锁住适当的数据)
①不用全局变量,而是在类中将mutex和受保护的数据组织在一起。
②P38不要将受保护数据的指针和引用传递到锁的范围之外,无论是通过从函数中返回它们、将其放在外部可见的内存中、还是作为参数传递给用户提供的函数。
2.死锁
(1)概念
死锁:线程在占有了一个资源时,还需要另外的资源才能完成一个操作。而两个以上的线程,互相在等待着别的线程占有的资源,整个系统陷入停顿状态。
注:死锁很可能发生在,多线程中每个线程需要锁定两个及以上的互斥元的情况,所以叫死锁。
但是也可能与mutex、lock无关。比如,两个线程互相join对方,等待对方终止,而自己不会先终止,这就会陷入死锁。
(2)解决办法
A。同时锁定
可以使用std::lock()同时锁定两个互斥元。
B。分别获取锁
①避免嵌套锁
即一个线程最多持有一个锁,这个时候是不可能光凭锁产生死锁的。
②在持有锁时,避免使用用户提供的代码
这是作为①的补充,因为用户可能做任何事情,包括加锁,所以如果你已经持有了一个锁,不使用任何可能再加锁的代码,就能避免由于加锁而引起的死锁。
③以固定顺序获取锁
在每个线程中以相同的顺序获取锁。
④使用层次锁
在锁建立的时候,为每种锁确定一个层次值,这个值到后面会允许,低的层次可以在高层次的基础上继续上锁,但反之不行。同时同层的不能再锁。
3.提高并发性能
高效使用锁的核心是:锁定在恰当的粒度,只应该以执行要求操作所需的最小可能时间去持有锁。
还可以在恰当的地方不使用锁而使用mutex提供的其他类,或者是使用非标准库,比如boost里面的类。
(1)std::unique_lock
这个类似于std::lock_guard只是可以进行更灵活的操作,比如加了锁,又解锁,然后又加,以及转移锁的使用权等。
代价是需要比std::lock_guard存储更多的信息。
(2)std::once_flag,std::call_once
有些应用对象在创建之后,只用于读,于是就不需要加锁了。
唯一需要加锁的地方就是,在初始化的时候,避免多个线程都以为自己是第一个对该对象进行初始化。
而如果使用mutex来先判断初始化没有,然后根据判断来做相应的事情,确实在第一次的时候,很好滴实现了只有一个线程初始化。
可是在之后,所有的线程进行判定的时候,锁还在那里,会形成不必要的序列化。
这个时候,对于只需要在初始化时保护共享数据的情况,就可以使用者两个类了。
(3)读者写者问题(保护很少更新的数据结构)
读和写应该是互斥的,但是所有的读操作又互不影响。
可以采用共享锁。
将所有读的加共享锁,将写的加独占锁。
这样,读的人互不冲突,但是会和写的人互斥。
而写的人和其他人都互斥,不管是和一个想要读的人,还是和另一个想要写的人。
11/03补充:
4.unique_lock和lock_guard的比较
(1)代价P51
允许unique_lock实例不拥有互斥元的灵活性是有代价的,这条信息必须被存储,并且必须被更新。
(2)好处P52
①延迟锁定
②锁的所有权从一个作用域转移到另一个作用域
注:这个开始我还没有重视,其实这个就是说unique_lock是允许move操作的,这样它可以return出一个函数而不被析构,而可以用一个move函数来移动构造。
这个在高度并发环境是可以带来较大性能提升的。
相反lock_guard的构造函数是不支持移动构造的。
③随时锁定,随时解锁