《C++并发编程实战》读书笔记4---并发数据结构queue

来源:互联网 发布:m9军刀多少钱淘宝 编辑:程序博客网 时间:2024/06/14 15:07

对应于书中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类型的实例,可能代价昂贵,可能构造需要的参数在此处并不可用。

其次还必须支持可赋值的,这是一个重要限制。

所以提供了两种重载类型,而②比①更好一点。









0 0
原创粉丝点击