C++ 多线程 线程安全队列设计
创始人
2024-02-02 06:44:41

这是看《C++并发编程实战》这本书学的,这里我要为这本书辟谣一下,虽然是这本书前面翻译得很烂,但是从第6章开始,应该是换了个人翻译,虽然还是能难懂,但是难懂的是代码逻辑,而不是语言逻辑;

实现,我先说明一下我自己的一个感悟,即对大多数线程错误的感悟:

1:我设定一个“信息有效期”这个概念,这个概念是值我从一个信息源获取一个信息,这个信息相对这个信息源是正确的时间段,比如一个线程获取一个队列是否为空,如果得到的结果是true,那么从获取这个信息到队列中添加一个元素这段时间就是获取到的信息的有效期;

2:大多数线程错误都是来源于信息有效期已经过去,我们知道,算法他的每一个步骤都是依赖之前的步骤的,如果前一个步骤的结果到实现依赖这个步骤的步骤时是错误的,那么不能说绝对,只能说是很可能这个步骤执行完后的结果也是错误的,那么这个“错误链”就会传递下去,如果没有正确的异常处理,那么这个程序很可能就会崩溃;

3:怎么避免信息有效期过去?

        -:获得的信息有效期够长

        -:利用锁来将信息有效期延长;

        -:获得的信息正确与否不重要,我们只关系信息本身【这个也可能是无锁并发的实现思想之一】

4:细粒度,如果我们要一个多线程安全的数据结构效率高的话,我们一般是需要控制好锁保护的数据范围,一般来说,一个锁保护的数据越多,那么这个数据结构并发访问的效率就越低,相反则反,另外一个方面,当一个操作需要的锁越多,那么并发访问的效率就越低,相反则反;

粗粒度队列的设计

一般我们是怎么设计一个队列的呢,利用锁将单线程安全队列变为多线程安全队列?

直接用一个单链表就行了;

 我们只需要用两个互斥量来保护head和tail的是串行的即可;

template
class queue
{
private:struct node{T data;std::unique_ptr next;node(T data_) :data(std::move(data_)){}};std::unique_ptr head; // 1node* tail; // 2mutex head_mutex, tail_mutex;
public:queue(){}queue(const queue& other) = delete;queue& operator=(const queue& other) = delete;std::shared_ptr try_pop(){lock_guard lock_g(head_mutex);if (!head){return std::shared_ptr();}std::shared_ptr const res(std::make_shared(std::move(head->data)));std::unique_ptr const old_head = std::move(head);head = std::move(old_head->next); // 3return res;}void push(T new_value){lock_guard lock_g(tail_mutex);lock_guard lock_g(head_mutex);std::unique_ptr p(new node(std::move(new_value)));node* const new_tail = p.get();if (tail){tail->next = std::move(p); // 4}else{head = std::move(p); // 5}tail = new_tail; // 6}
};

这里我们可以看到,每一个锁在保护数据的数量上,已经是非常少了【每一个锁只保护了一个节点,这已经是非常理想的状态了】,那么我们可以看到在try_pop操作当中,我们只依赖与tail节点,而在push操作当中,我们依赖head和tail两个节点,那么我们能不能进行修改,让push只依赖一个节点呢?答案是:能;

细粒度队列的设计

我们只需要一个虚拟节点即可,这个节点我们保证一直在队列的最后,且队列稳定的时候,tail恒定指向这个虚拟节点,这样的话,当我们push的时候,我们就可以不用依赖与head节点了,因为无论是空队列还是非空队列,在执行push操作的前后,head的指向都不变,这意味着我们可以不访问head节点;

我们在构造队列的时候,我们创建一个虚拟节点,然后让head和tail都指向这个节点,要稍微注意的是,判空条件不再是head==nullptr了,而是head.get()==tail;

看具体代码:

class Queue {
public:class node {public:shared_ptr data;unique_ptr next;};
private:unique_ptr head;node* tail;mutex tail_mtx, head_mtx;unique_ptr pop_head() {std::lock_guard lock_g(head_mtx);if (head.get() == get_tail()) {return nullptr;}unique_ptr old_head = std::move(head);head = std::move(old_head->next);return old_head;}node* get_tail() {std::lock_guard lock_g(tail_mtx);return tail;}public:Queue():head(std::make_unique()), tail(head.get()){}void push(int new_date) {shared_ptr date = std::make_shared(new_date);unique_ptr new_tail = std::make_unique();node* temp = new_tail.get();std::lock_guard lock_g(tail_mtx);tail->data = date;tail->next = std::move(new_tail);tail = temp;}std::shared_ptr try_pop() {unique_ptr ptr = pop_head();return ptr == nullptr ? nullptr : ptr->data;}
};

那么好,这个时候这个队列的并发访问性已经是非常高了【主要是队列能够操作的自由度是真不高】

那么好,我们现在再提出一个需求,有的时候,我们一个线程必须要从一个队列中获取一个元素,才能进行下面的操作【比如线程池】,而我们不希望用一个while循环来进行检查队列中是否有元素以供获取,这个时候我们可以通过环境变量来进行,但是我们希望这个wait的过程与实际的业务代码是低耦合的,所以我们要把,这个逻辑封装到队列中;

这里我们可以通过两种方式来获取队列中的值,一个是用引用hook,一个是直接通过返回值来获取;

当然,我们用引用hook时,就不免会用到客户端程序员的带代码,移动拷贝结果的时候可能会报错,这个时候需要对具体的异常进行捕获,为了避免这个异常,我们设计节点用share_ptr来存储结果,因为share_ptr内部在创建对象的时候有异常处理,但是这样做的坏处是我们分配的结果必须存储到堆中,这为push时创建结果的过程增加了不少的时间开销;

下面是具体代码:

template
class queue_plus {
private:class node {public:std::shared_ptr date;std::unique_ptr next;};mutex head_mtx, tail_mtx, size_mtx;unique_ptr head;node* tail;condition_variable cond;node* get_tail() {std::lock_guard lock_g(tail_mtx);return tail;}shared_ptr pop_head() {if (head->get() == get_tail()) {return nullptr;}shared_ptr date=std::move(head->date);head = std::move(head->next);return date;}unique_lock wait_for_notify() {std::unique_lock head_lock(head_mtx);cond.wait(head_lock, [&] {head.get() != get_tail(); });return head_lock;}void wait_for_date(T& value) {std::unique_lock head_lock(wait_for_notify());shared_ptr date = pop_head();if (date != nullptr) {value = std::move(*date);}}shared_ptr wait_for_date() {std::unique_lock head_lock(wait_for_notify());return pop_head();}
public:void wait_and_pop(T& vlaue) {wait_for_date(value);}shared_ptr wait_and_pop() {return wait_for_date();}};

相关内容

热门资讯

应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...