对应于书中6.1-6.2节的内容,主要是应用清单6.1-6.10共7个程序来说明问题的。
(1)6.1-6.3线程安全栈与队列
(2)6.4-6.6控制数据结构详细实现的细粒度锁定
(3)6.7-6.10最终作品
1.线程安全栈与队列
6.1线程安全栈
(1)函数成员
1个默认构造函数
1个拷贝构造函数
删除拷贝赋值运算符
1个push
2个pop
1个empty
(2)细节
①在pop时,若stack为空,则throw异常
②每一个操作都是使用一个mutex全程锁定
6.2线程安全队列
(1)函数成员
1个默认构造
1个push
2个try_pop
2个wait_and_pop
1个empty
(2)细节
①模仿6.1的栈构建的线程安全队列,所以所有操作都全程锁定mutex
(3)与6.1差异
①加入了条件变量(condition_variable),在每次push的时候notify,拥有wait_and_pop在pop的时候wait
②在try_pop中,如果queue为空不抛出异常,而是返回成功与否(输出参数的重载),或者返回空指针(返回智能指针的重载)。
6.3包含shared_ptr<>的线程安全队列
(1)函数成员
同6.2
(2)细节
①在queue里面不是直接放的<T>类型的数据,而是指向T数据的智能指针
(3)与6.2差异
①在push的时候,且在于锁的外面,实现新加入元素的内存分配。
注:这样做的好处
①将申请智能指针的操作,从pop移动到push里面,这样保证了在wait_and_pop中因为构造智能指针抛出异常时,总有一个线程被唤醒。
因为构造智能指针的步骤已经移动到push里面去了。
②使得构造智能指针的过程,可以不在锁定之中,提升了性能。
2.控制数据结构,使用细粒度
6.4用list实现queue
(1)函数成员
1个默认构造
删除拷贝构造
删除拷贝赋值
1个try_pop
1个push
(2)细节(3)与6.3不同
①由于使用list许多node来构造,所以新建数据结构node,node里面有data和next,而外面还有head和tail
②其中tail是node的一般指针,而head是node的unique_ptr。
因为queue是从head弹出数据,所以把head定义为unique_ptr可以保证节点被自动而方便地删除。
注:实现head和tail两个数据项的最终目的,还是将push,和pop两种操作的mutex分开,减少不必要的序列化,提高性能。
6.5引入傀儡节点的队列
(1)函数成员
同6.4
(2)细节(3)与6.4不同
①一开始默认初始化时就在queue里面放入一个没有data的node
注:这样做是为了避免在没有节点时,锁定push和try_pop会锁定同一个互斥元,性能没有提升。
这样做的后果是,你push的时候,必须将数据插入当前的tail,然后新建一个new_tail然后,在连到tail后面。
6.6使用细粒度锁
(1)成员函数
同6.5
不过增加了get_tail()和pop_head()两个辅助函数
(2)细节(3)与6.5不同
①采用了两把锁,分别可以锁head和锁tail,对应于pop和push操作。
②使用辅助函数,将锁定操作放在辅助函数中,有两个好处:
A。可以使用return将unique_ptr的控制权传出来,然后再销毁。
也就是说费时的销毁操作,并不在锁定范围之内,锁定时的操作只是指针的赋值,提高了性能。
B。通过pop_head再去调用get_tail,可以保证需要持有两个锁时,总是有固定的锁定顺序,
避免了死锁的发生。
3.最终实现版本
6.7-6.10
(1)函数成员
1个默认构造函数
删除拷贝构造函数
删除拷贝赋值操作
2个wait_and_pop
2个try_pop
1个push
1个empty
辅助函数:
get_tail
pop_head
wait_for_data
2个wait_pop_head
注:①将wait_and_pop的操作分解为了wait_for_data和pop_head两步,而pop_head可以供给所有的pop重用。
②接着使用2个wait_pop_head将wait_for_data和pop_head组合起来,对应于2个wait_and_pop的不同版本。
之所以有这样一步,不直接使用2个wait_and_pop将其包起来,还是那个原因,将耗时的销毁unique_ptr操作放在锁的外面,提高性能
(2)区分4个pop函数
①try_pop与wait_and_pop的区别
try就是试着去弹,如果有就弹,没有就返回空bool的false或者返回空指针。
wait_and_pop就是使用条件变量,如果有就弹,没有就阻塞在wait那里,直到有或者一定的时间。
②shared_ptr<T> pop()和void pop(T& value)两种重载方式的区别
shared_ptr<T> pop()是返回弹出的数据的指针。
另一种是使用输出参数,将弹出的值直接放在调用者给的参数里面。
注:探究shared_ptr<T> pop()和void pop(T& value)形式存在的渊源,以及如何使用这两种
原因来自于本书3.2.3P40
本来应该的操作就是弹出与复制数据为同一个操作。
但是,仅在栈被修改后,出栈值才返回给调用者,但复制数据以返回给调用者的过程可能会发生异常。
一旦异常发生,刚从栈中出栈的数据会丢失,但复制没有成功。
于是std::stack接口的设计者,笼统地将操作一分为二:top()和pop(),先获取顶部元素(top)再从栈中删除(pop)。
期望可以当复制不成功时,数据仍留在栈上。
但是这么做的坏处就在于,在多线程并发时,这种接口存在固有的竞争条件。
对于这种接口竞争条件,解决办法有2种:
①传入引用,作为输出参数。---------->产生了void pop(T& value)
②返回指向栈顶的智能指针。---------->产生了shared_ptr<T> pop()
但是①有诸多限制,必须先构造一个T类型的实例,可能代价昂贵,可能构造需要的参数在此处并不可用。
其次还必须支持可赋值的,这是一个重要限制。
所以提供了两种重载类型,而②比①更好一点。