并发编程(15)——基于同步方式的线程安全的栈和队列

文章目录

  • 十四、day14
  • 1. 线程安全的栈
    • 1.1 存在隐患的栈容器
    • 1.2 优化后的栈容器
  • 2. 线程安全的队列
    • 2.1 基于智能指针的线程安全的队列
    • 2.2 不同互斥量管理队首、队尾的队列

十四、day14

在并发编程(1)并发编程(5)中,我们学习了关于线程管控的一些知识;在并发编程(6)并发编程(8)中,我们学习了关于async的一些知识;在并发编程(10)~并发编程(14)中,我们学习了关于原子、内存序的知识。

从本节开始,我们学习如何通过前五节的知识构建基于锁实现的线程安全队列、栈、链表和查找表等;并通过10~14节实现对应无锁版本的队列、栈、链表等。

我们在前面其实已经实现了一个基于锁的线程安全的队列和栈,但是有一些问题;而且实现了无锁和有锁版本的环形队列,环形队列的实现基于线程安全,无安全隐患。

因为栈和队列有一些相似之处,我们从栈开始学习如何基于锁实现线程安全,并尝试实现无隐患的线程安全的栈和队列。从有锁开始逐渐向无锁演变。

参考:

恋恋风辰官方博客

C++并发编程实战(第2版)


1. 线程安全的栈

1.1 存在隐患的栈容器

在文章并发编程(3)——锁(上) | 爱吃土豆的个人博客中我们提到了如何保护共享数据(切勿将受保护数据的指针或引用传递到互斥量作用域之外),此外我们实现了一个基于锁的线程安全的栈容器,支持数据的压入和弹出,代码如下:

struct empty_stack : std::exception
{const char* what() const throw();
};template<typename T>
class threadsafe_stack
{
private:std::stack<T> data;mutable std::mutex m;
public:threadsafe_stack() {}threadsafe_stack(const threadsafe_stack& other){std::lock_guard<std::mutex> lock(other.m);//①在构造函数的函数体(constructor body)内进行复制操作data = other.data;   }threadsafe_stack& operator=(const threadsafe_stack&) = delete;void push(T new_value){std::lock_guard<std::mutex> lock(m);data.push(std::move(new_value));}std::shared_ptr<T> pop(){std::lock_guard<std::mutex> lock(m);//②试图弹出前检查是否为空栈if (data.empty()) throw empty_stack();//③改动栈容器前设置返回值std::shared_ptr<T> const res(std::make_shared<T>(std::move(data.top()));    data.pop();return res;}void pop(T& value){std::lock_guard<std::mutex> lock(m);if (data.empty()) throw empty_stack();value = data.top();data.pop();}bool empty() const{std::lock_guard<std::mutex> lock(m);return data.empty();}
};

该栈容器解决了我们在并发编程(3)——锁(上) | 爱吃土豆的个人博客文章中提到的两个问题:

  1. 如果我们启动两个线程同时push一个元素,假设线程t1先启动,t1判断栈不为空,然后睡眠1s;t2在t1睡眠的时候也判断栈不为空,然后休息1s;当t1醒来之后将栈内唯一的元素pop出,成功执行;当t2醒来之后栈内已经没有元素,但因为t2之前判断栈不为空所以t2也会pop元素,此时就会报错,因为栈内已经没有元素可以pop了。

    解决方法:定义一个空栈异常,当栈在pop的时候并不像之前直接将元素取出,而是先判断栈是否为空,如果栈为空但仍然调用了pop函数,那么就弹出empty_stack()异常。但如果这么做的话,就需要我们在外层调用pop的时候使用catch捕获异常,进行相应的处理。但异常一般来说适用于处理和 预判突发情况的,对于一个栈为空这种常见现象,仅需根据返回之后判断为空再做尝试或放弃出栈即可,没必要返回异常。

struct empty_stack : std::exception
{const char* what() const throw();
};
  1. 当程序内存暴涨或者数据本身占据过大内存时,pop可能会造成数据丢失问题,具体内容可参考并发编程(3)——锁(上) | 爱吃土豆的个人博客

    解决方法:基于以下原理重载两个版本的pop函数:*先用top() 获取栈顶元素,随后再用pop() 将元素移出栈容器(也就是先进行拷贝,后pop,这样报错也就只能在pop前面复制的步骤发生),这样就保障了数据安全,即使我们无法安全地复制数据,数据仍然保留在栈中。*

    第一个版本是带引用类型的参数(直接将元素传递给引用参数,引用参数已经在外界开辟好了内存空间,不存在内存不足的问题);

    第二个版本是将pop出的元素封装成智能指针类型(将元素top传入调用智能指针res的拷贝构造函数,即使因为内存过大报错,但元素仍在栈中),然后返回(return的时候也只是返回智能指针,智能指针占用的内存不大,我们在外层只需解引用return的智能指针,即可使用栈pop出的内容)。

我们在文章的一开始说过,虽然该栈容器解决了以上两个问题,可以在多线程并发下线程安全的运行,但是有一些隐患

  1. 尽管上面的例子在多线程并发调用中是线程安全的,但锁的排他性仅容许一次只有一个线程访问栈中的数据,这会让其他线程可能为了获取锁而陷入等待,从而浪费资源。
  2. 该栈容器没有提供任何 “等待/添加数据” 的操作,假如栈容器已经没有多余空间容纳其他数据,而其他线程又等着添加数据,那么此线程就必须反复调用 empty() 函数,或者调用 pop() 函数判断栈是否为空,这是极为浪费算力资源的事情。

1.2 优化后的栈容器

我们可以优化代码如下:

template<typename  T>
class threadsafe_stack_waitable
{
private:std::stack<T> data;mutable std::mutex m;std::condition_variable cv;
public:threadsafe_stack_waitable() {}threadsafe_stack_waitable(const threadsafe_stack_waitable& other){std::lock_guard<std::mutex> lock(other.m);data = other.data;}threadsafe_stack_waitable& operator=(const threadsafe_stack_waitable&) = delete;void push(T new_value){std::lock_guard<std::mutex> lock(m);data.push(std::move(new_value));    // ⇽-- - 1cv.notify_one();}std::shared_ptr<T> wait_and_pop(){std::unique_lock<std::mutex> lock(m);cv.wait(lock, [this]()   {if(data.empty()){return false;}return true;}); //  ⇽-- - 2std::shared_ptr<T> const res(std::make_shared<T>(std::move(data.top())));   // ⇽-- - 3data.pop();   // ⇽-- - 4return res;}void wait_and_pop(T& value){std::unique_lock<std::mutex> lock(m);cv.wait(lock, [this](){if (data.empty()){return false;}return true;});value = std::move(data.top());   // ⇽-- - 5data.pop();   // ⇽-- - 6}bool empty() const{std::lock_guard<std::mutex> lock(m);return data.empty();}bool try_pop(T& value){std::lock_guard<std::mutex> lock(m);if(data.empty()){return false;}value = std::move(data.top());data.pop();return true;}std::shared_ptr<T> try_pop(){std::lock_guard<std::mutex> lock(m);if(data.empty()){return std::shared_ptr<T>();}std::shared_ptr<T> res(std::make_shared<T>(std::move(data.top())));data.pop();return res;}
};

我们通过并发编程(5)——条件变量、线程安全队列 | 爱吃土豆的个人博客提到的条件变量优化代码。

像前面实现的栈容器一样,我们实现了两种pop函数(引用和智能指针),但是在本例中,每一种又分成了两类:try_pop版本和wait_and_pop版本

  • try_pop 保证线程不会阻塞等待,如果栈有数据那就返回数据,如果没有数据就直接返回false或者空指针(对应引用和智能指针的两个方式)
  • wait_and_pop 保证线程处于阻塞等待,如果栈有数据会返回数据,如果没有就通过条件变量挂起,释放持有的锁,直至该线程被唤醒。通过条件变量可以避免线程循环判断拿取数据,从而浪费资源。

虽然我们通过条件变量避免多线程并发时算力资源的浪费,但是该方式会引发另一个问题

假设栈此时为空,线程A从队列中消费数据并调用 wait_and_pop 进入挂起状态(如果栈没数据,调用该函数的线程会被挂起,直至其生产线程push数据进去)。与此同时,另一个线程B向栈中放入数据并调用 push 操作,随后通过 notify_one 随机唤醒挂起中的一个线程(如果多个线程调用wait_and_pop,但栈没有数据时,这些线程都会进入挂起状态)以便消费队列中的数据。

假如线程A从 wait_and_pop 被唤醒,如果在执行过程中(比如第3或第5步)因为内存不足引发异常(我们在第三节说过,如果数据本身太大,那么我们在调用拷贝构造时可能会因为内存不足而引发异常),我们之前分析过,即便出现异常,也不会影响栈中的数据,因此栈的数据仍然是安全的。然而,线程A一旦发生异常,其他线程就无法继续从队列中消费数据,除非线程B再次执行 push 操作。因为我们使用的是 notify_one,每次只能唤醒一个线程,因此如果被唤醒的线程A发生了异常,导致它无法继续执行,那么就没有其他线程能够消费栈中的数据。

我们可以通过以下三个方案解决上述问题:

  1. wait_and_pop 失败的线程修复后再次取数据
    如果线程在调用 wait_and_pop 时失败(例如因为内存不足),修复错误后让该线程再次尝试获取数据。

  2. notify_one 改为 notify_all
    通过将 notify_one 修改为 notify_all,可以确保所有等待的线程都能接收到通知并被唤醒。这样一来,多个线程都有机会继续执行,避免了单个线程失败后没有其他线程被唤醒的问题。然而,notify_all 会导致所有线程同时竞争资源,这可能导致性能问题和不必要的上下文切换,浪费资源。

  3. 使用智能指针存储栈数据
    通过在栈中存储智能指针,可以避免因内存分配失败而引发异常。智能指针在赋值过程中会进行内存管理,但不会像普通指针那样引发异常,如果内存不够,智能指针会返回一个空指针nullptr

我们这里使用第三种方式优化,一个无隐患且线程安全的的栈容器实现如下:

template<typename T>
class threadsafe_stack_waitable
{
private:std::stack<std::shared_ptr<T>> data;  // 使用智能指针存储栈数据mutable std::mutex m;std::condition_variable cv;
public:threadsafe_stack_waitable() {}threadsafe_stack_waitable(const threadsafe_stack_waitable& other){std::lock_guard<std::mutex> lock(other.m);data = other.data;}threadsafe_stack_waitable& operator=(const threadsafe_stack_waitable&) = delete;// 使用智能指针进行push操作void push(std::shared_ptr<T> new_value){std::lock_guard<std::mutex> lock(m);data.push(std::move(new_value));  // 直接使用shared_ptrcv.notify_one();}// 等待并弹出栈顶数据,返回值是智能指针std::shared_ptr<T> wait_and_pop(){std::unique_lock<std::mutex> lock(m);cv.wait(lock, [this]()   {return !data.empty();  // 如果栈非空,则返回true});// 返回栈顶元素并移除std::shared_ptr<T> const res = data.top();  // 不需要再次创建shared_ptr,栈本身存储的是智能指针data.pop();return res;}// 等待并将栈顶数据存入传递引用的valuevoid wait_and_pop(T& value){std::unique_lock<std::mutex> lock(m);cv.wait(lock, [this](){ return !data.empty(); });// 将栈顶数据转移给valuevalue = std::move(*data.top());data.pop();}// 检查栈是否为空bool empty() const{std::lock_guard<std::mutex> lock(m);return data.empty();}// 尝试弹出栈顶元素并存入传递引用的valuebool try_pop(T& value){std::lock_guard<std::mutex> lock(m);if(data.empty()){return false;}// 将栈顶元素转移到valuevalue = std::move(*data.top());data.pop();return true;}// 尝试弹出栈顶元素,返回一个智能指针std::shared_ptr<T> try_pop(){std::lock_guard<std::mutex> lock(m);if(data.empty()){return nullptr;  // 返回空指针}// 返回栈顶元素并移除std::shared_ptr<T> res = data.top();data.pop();return res;}
};

需要注意的是,实现返回智能指针的pop函数时,我们不需要将栈中的数据先构造一个智能指针后,然后提供给需要返回的智能指针;因为栈中存储的元素类型的智能指针,我们直接将数据返回即可。

// 优化前
std::shared_ptr<T> const res(std::make_shared<T>(std::move(data.top())));   // ⇽-- - 3
// 优化后         
std::shared_ptr<T> const res = data.top();  // 不需要再次创建shared_ptr,栈本身存储的是智能指针

2. 线程安全的队列

2.1 基于智能指针的线程安全的队列

我们在文章并发编程(5)——条件变量、线程安全队列 | 爱吃土豆的个人博客中通过条件变量实现了一个线程安全的队列,但我们通过条件变量实现的生产-消费者队列同样有上面通过条件变量实现的栈容器的问题

假设栈此时为空,线程A从队列中消费数据并调用 wait_and_pop 进入挂起状态(如果栈没数据,调用该函数的线程会被挂起,直至其生产线程push数据进去)。与此同时,另一个线程B向栈中放入数据并调用 push 操作,随后通过 notify_one 随机唤醒挂起中的一个线程(如果多个线程调用wait_and_pop,但栈没有数据时,这些线程都会进入挂起状态)以便消费队列中的数据。

假如线程A从 wait_and_pop 被唤醒,如果在执行过程中(比如第3或第5步)因为内存不足引发异常(我们在第三节说过,如果数据本身太大,那么我们在调用拷贝构造时可能会因为内存不足而引发异常),我们之前分析过,即便出现异常,也不会影响栈中的数据,因此栈的数据仍然是安全的。然而,线程A一旦发生异常,其他线程就无法继续从队列中消费数据,除非线程B再次执行 push 操作。因为我们使用的是 notify_one,每次只能唤醒一个线程,因此如果被唤醒的线程A发生了异常,导致它无法继续执行,那么就没有其他线程能够消费栈中的数据。

我们之前通过条件变量实现的线程安全的队列代码如下:

#include <queue>
#include <mutex>
#include <condition_variable>template<typename T>
class threadsafe_queue
{
private:mutable std::mutex mut;    std::queue<T> data_queue;std::condition_variable data_cond;
public:threadsafe_queue(){}threadsafe_queue(const threadsafe_queue& other){std::lock_guard<std::mutex> lk(other.mut);data_queue = other.data_queue;}void push(T new_value){std::lock_guard<std::mutex> lk(mut);data_queue.push(new_value);data_cond.notify_one();}// 返回引用void wait_and_pop(T& value){std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this] {return !data_queue.empty(); });value = data_queue.front();data_queue.pop();}// 返回智能指针std::shared_ptr<T> wait_and_pop(){std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this] {return !data_queue.empty(); });std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));data_queue.pop();return res;}// 返回引用bool try_pop(T& value){std::lock_guard<std::mutex> lk(mut);if (data_queue.empty())return false;value = data_queue.front();data_queue.pop();return true;}// 返回智能指针std::shared_ptr<T> try_pop(){std::lock_guard<std::mutex> lk(mut);if (data_queue.empty())return std::shared_ptr<T>();std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));data_queue.pop();return res;}bool empty() const{std::lock_guard<std::mutex> lk(mut);return data_queue.empty();}
};

选择将队列元素类型定义为智能指针,从而避免拷贝时内存不足引发的异常问题(智能指针在赋值过程中会进行内存管理,但不会像普通指针那样引发异常,如果内存不够,智能指针会返回一个空指针nullptr),优化后的代码如下:

template<typename T>
class threadsafe_queue_ptr
{
private:mutable std::mutex mut;std::queue<std::shared_ptr<T>> data_queue;std::condition_variable data_cond;
public:threadsafe_queue_ptr(){}threadsafe_queue_ptr(const threadsafe_queue_ptr& other){std::lock_guard<std::mutex> lk(other.mut);data_queue = other.data_queue;}void wait_and_pop(T& value){std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this] {return !data_queue.empty(); });value = std::move(*data_queue.front());    //⇽-- - 1data_queue.pop();}bool try_pop(T& value){std::lock_guard<std::mutex> lk(mut);if (data_queue.empty())return false;value = std::move(*data_queue.front());   // ⇽-- - 2data_queue.pop();return true;}std::shared_ptr<T> wait_and_pop(){std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this] {return !data_queue.empty(); });std::shared_ptr<T> res = data_queue.front();   // ⇽-- - 3data_queue.pop();return res;}std::shared_ptr<T> try_pop(){std::lock_guard<std::mutex> lk(mut);if (data_queue.empty())return std::shared_ptr<T>();std::shared_ptr<T> res = data_queue.front();   // ⇽-- - 4data_queue.pop();return res;}void push(T new_value){std::shared_ptr<T> data(std::make_shared<T>(std::move(new_value)));   // ⇽-- - 5std::lock_guard<std::mutex> lk(mut);data_queue.push(data);data_cond.notify_one();}bool empty() const{std::lock_guard<std::mutex> lk(mut);return data_queue.empty();}
};

其实和优化前的版本主要区别在元素类型上,优化前队列的元素类型是 T ,优化后队列的元素类型是智能指针。我们选择智能指针是为了避免普通指针因为内存分配而引发的异常,所以我们还需要对优化前的代码中关于内存分配的部分进行修改:

  1. push 数据时需要先构造指针智能,然后将构造的指针指针压入队列中,这样可以避免先前代码可能会因为内存不足而引发异常,从而污染队列中的数据。

    // 优化前
    void push(T new_value)
    {std::lock_guard<std::mutex> lk(mut);data_queue.push(new_value);data_cond.notify_one();
    }
    // 优化后
    void push(T new_value)
    {std::shared_ptr<T> data(std::make_shared<T>(std::move(new_value)));   // 先构造智能指针std::lock_guard<std::mutex> lk(mut);data_queue.push(data);data_cond.notify_one();
    }
    
  2. 智能指针在调用构造函数时可能会因为内存不足而导致失败(但不会导致异常,智能指针会返回一个空指针nullptr),而智能指针间的赋值并不会引发异常,因为智能指针间的赋值不涉及内存的分配。

    赋值操作不导致内存分配是因为智能指针的赋值通常是将一个已经分配的资源所有权转移或引用计数更新,而不是重新分配内存。例如,当你将一个智能指针赋值给另一个智能指针时,智能指针会复制原指针的资源管理权或增加引用计数,而不是重新分配内存或创建新的资源。因此,内存分配只在构造新的智能指针或创建新的资源时发生,而赋值操作只是管理已存在的资源。

    其实创建一个智能指针共享资源也会造成一定程度的内存开销,只不过很少,因为智能指针在栈上开辟,占8字节,所以每创建一个新的智能指针,便会在栈上开辟8字节的空间。

2.2 不同互斥量管理队首、队尾的队列

队列和栈最大的区别就是:栈是先入后出的,队列是先入先出的,如下图

在这里插入图片描述

栈:先入后出

在这里插入图片描述

队列:先入先出

在上面的代码中,push 和 pop 函数施加同步是用的同一个mutex,这会导致 push 和 pop 在多线程环境中串行化,即,一个线程执行 push ,那么其他线程就没办法执行 pop 或 push,虽然我们通过条件变量挂起了这些线程,避免了算力资源的浪费,但是我们没有办法实现 push 和 pop 的同时调用。

在库中,队列一般是通过双向链表实现的,首节点和尾节点分别进行出队和入队操作,我们可以考虑对队首和队尾分别使用不同的互斥量进行管理,实现真正的并发,而不是在同一时刻有且仅有一个线程实现一个操作。

要想自己实现一个基于链表的队列,需满足以下操作:

  1. 定义一个虚拟头节点,没有任何数据,队列为空时头节点和尾节点均指向虚拟头节点。
  2. 执行 push 操作时,将尾指针指向的节点的 next 指针指向新加入的节点,并更新尾指针使其指向新节点。
  3. 执行 pop 操作时,先检查队列是否为空,在满足条件的情况下,更新头指针使其指向下一个节点。

这部分是链表的内容,我就不详细说明了,可以在力扣上完成这道题:707. 设计链表 - 力扣(LeetCode)熟悉链表。

template<typename T>
class threadsafe_queue_ht
{
private:struct node{std::shared_ptr<T> data;std::unique_ptr<node> next;};std::mutex head_mutex; std::unique_ptr<node> head; // std::unique_ptr<node> 和 node* 都可以std::mutex tail_mutex;node* tail;std::condition_variable data_cond;node* get_tail(){std::lock_guard<std::mutex> tail_lock(tail_mutex);return tail;}std::unique_ptr<node> pop_head()   {	// 将头指针更新至当前头指针的next指针,并返回旧的头指针std::unique_ptr<node> old_head = std::move(head);head = std::move(old_head->next);return old_head;}std::unique_lock<std::mutex> wait_for_data()   {std::unique_lock<std::mutex> head_lock(head_mutex);// 判断头和尾是否相同,相同即为空队列data_cond.wait(head_lock,[&] {return head.get() != get_tail(); }); //5return std::move(head_lock);   }std::unique_ptr<node> wait_pop_head(){// 判断队列是否为空,并获取不为空时的 head_lockstd::unique_lock<std::mutex> head_lock(wait_for_data());   return pop_head();}std::unique_ptr<node> wait_pop_head(T& value){std::unique_lock<std::mutex> head_lock(wait_for_data());  value = std::move(*head->data);return pop_head();}std::unique_ptr<node> try_pop_head(){std::lock_guard<std::mutex> head_lock(head_mutex);if (head.get() == get_tail()){return std::unique_ptr<node>();}return pop_head();}std::unique_ptr<node> try_pop_head(T& value){std::lock_guard<std::mutex> head_lock(head_mutex);if (head.get() == get_tail()){return std::unique_ptr<node>();}value = std::move(*head->data);return pop_head();}
public:threadsafe_queue_ht() :  // ⇽-- - 1head(new node), tail(head.get()){}threadsafe_queue_ht(const threadsafe_queue_ht& other) = delete;threadsafe_queue_ht& operator=(const threadsafe_queue_ht& other) = delete;std::shared_ptr<T> wait_and_pop() //  <------3{std::unique_ptr<node> const old_head = wait_pop_head();return old_head->data;}void wait_and_pop(T& value)  //  <------4{std::unique_ptr<node> const old_head = wait_pop_head(value);}std::shared_ptr<T> try_pop(){std::unique_ptr<node> old_head = try_pop_head();return old_head ? old_head->data : std::shared_ptr<T>();}bool try_pop(T& value){std::unique_ptr<node> const old_head = try_pop_head(value);return old_head;}bool empty(){std::lock_guard<std::mutex> head_lock(head_mutex);return (head.get() == get_tail());}void push(T new_value)  //<------2{std::shared_ptr<T> new_data(std::make_shared<T>(std::move(new_value)));std::unique_ptr<node> p(new node);node* const new_tail = p.get();std::lock_guard<std::mutex> tail_lock(tail_mutex);tail->data = new_data;tail->next = std::move(p); tail = new_tail; // 更新尾指针data_cond.notify_one();}
};

节点我们通过C++的方式实现:数据使用 shared_ptr 构造,可以避免一些异常错误,next 指针通过 unique_ptr实现,std::unique_ptr<node>和node*的效果一样。

struct node
{std::shared_ptr<T> data;std::unique_ptr<node> next;
};

此外,head 指针使用了 std::unique_ptr<node> 进行构造,tail 指针使用了 node*,所以构造函数 1 中的操作是正确的:

threadsafe_queue_ht() : head(new node), tail(head.get()){}

我们可以使用指针指针通过 get 函数获取的原始指针构造普通指针,但不能构造其他智能指针,否则会导致所有权转移或者引用计数独立的问题。比如使用head的原始指针构造另一个unique_ptr,那么head的所有权会被转移给构造的unique_ptr。如果构造的是另一个shared_ptr,并且head也是shared_ptr,那么两个shared_ptr不共享引用计数,二者是独立的。

在 push 函数中,流程如下:

  1. 先构造一个T类型的智能指针存储push的数据new_data;
  2. 然后构造一个新的节点 p;
  3. 获取p的裸指针new_tail,用于更新队列的尾部,p此时仍然管理这个裸指针new_tail;
  4. 将新的数据存入当前尾指针指向的节点,然后将新节点 p 的所有权转移给当前尾指针的 next 指针;
  5. 最后将尾指针更新为新的尾部new_tail。这里 new_tail 是指向新创建节点的裸指针,而 p 现在是空的,因为它的所有权已被转移。(新的尾指针data和next均为空)

数据使用 std::shared_ptr 构造,next 指针使用 std::unique_ptr 构造

3,4处都是wait_and_pop的不同版本,内部调用了wait_pop_head,wait_pop_head内部先调用wait_for_data判断队列是否为空,这里判断是否为空主要是判断head是否指向虚位节点。如果不为空则返回unique_lock,我们显示的调用了move操作,返回unique_lock仍保留对互斥量的锁住状态。最后调用pop_head将头数据pop出来。

值得注意的是get_tail()返回tail节点,tail 可能在不同的线程中被更新,导致 get_tail() 获取的尾节点并不是最终的尾节点。我们此时在5处的判断可能是基于push之前的tail信息,但是不影响逻辑,因为如果head和tail相等则线程挂起,等待通知,如果不等则继续执行,push操作只会将tail向后移动不会导致逻辑问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/484646.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

容器第五天(day042)

1.安装 yum install -y docker-compose 2.配置 配置文件名字&#xff1a;docker-compose.yaml或docker-compose.yml 3.启动 docker-compose up -d

离散数学重点复习

第一章.集合论 概念 1.集合是不能精确定义的基本数学概念.通常是由指定范围内的满足给定条件的所有对象聚集在一起构成的 2.制定范围内的每一个对象称为这个集合的元素 3.固定符号如下: N:自然数集合 Z:整数集合 Q:有理数集合 R:实数集合 C:复数集合 4.集合中的元素是…

docker学习笔记(四)--DockerFile

文章目录 一、什么是Dockerfile二、docker build命令三、dockerfile指令3.1 FROM3.2 ENV3.3 WORKDIR3.4 RUN3.5 CMD3.6 ENTRYPOINT3.7 EXPOSE3.8 ARG3.9 ADD3.10 COPY3.11 VOLUME 四、dockerfile示例 一、什么是Dockerfile Dockerfile 是用于构建 Docker 镜像的脚本文件&#…

动手学深度学习-线性神经网络-1线性回归

目录 线性回归的基本元素 线性模型 损失函数 解析解 随机梯度下降 用模型进行预测 矢量化加速 正态分布与平方损失 从线性回归到深度网络 神经网络图 生物学 小结 回归&#xff08;regression&#xff09;是能为一个或多个自变量与因变量之间关系建模的一类方法。…

BERT模型的输出格式探究以及提取出BERT 模型的CLS表示,last_hidden_state[:, 0, :]用于提取每个句子的CLS向量表示

说在前面 最近使用自己的数据集对bert-base-uncased进行了二次预训练&#xff0c;只使用了MLM任务&#xff0c;发现在加载训练好的模型进行输出CLS表示用于下游任务时&#xff0c;同一个句子的输出CLS表示都不一样&#xff0c;并且控制台输出以下警告信息。说是没有这些权重。…

设计模式:18、组合模式

目录 0、定义 1、组合模式的三种角色 2、组合模式的UML类图 3、示例代码 0、定义 将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使用户对单个对象和组合对象的使用具有一致性。 1、组合模式的三种角色 抽象组件&#xff08;Component&#xff09;&#…

Canal 深入解析:从原理到实践的全面解读

Canal 深入解析&#xff1a;从原理到实践的全面解读 官网&#xff1a;https://github.com/alibaba/canal Canal 是阿里巴巴开源的一款分布式增量数据同步工具&#xff0c;广泛应用于数据同步、实时数据处理和数据库的增量备份等场景。它可以通过监听 MySQL 数据库的 binlog&am…

LCD1602液晶显示屏指令详解

文章目录 LCD1602液晶显示屏1.简介2. 液晶引脚说明3. 指令介绍3.1 清屏指令3.2 光标归位指令3.3 进入模式设置指令3.4 显示开关设置指令3.5 设定显示或光标移动方向指令3.6 功能设定指令3.7 设定CGRAM地址指令3.8 设定DDRAM地址指令3.9 读取忙或AC地址指令3.10 总图3.11 DDRAM …

阿里云中Flink提交作业

访问阿里云首页面&#xff1a;https://www.aliyun.com/ 选择"按量付费" 通过选择区域&#xff0c;看哪个区域有虚拟交换机。 查看创建的工作空间&#xff0c;当工作空间状态为运行中时&#xff0c;点击控制台。 开通完成后&#xff0c;会有一个控制台&#xff1a; 可…

【不稳定的BUG】__scrt_is_managed_app()中断

【不稳定的BUG】__scrt_is_managed_app函数中断 参考问题详细的情况临时解决方案 参考 发现出现同样问题的文章: 代码运行完所有功能&#xff0c;仍然会中断 问题详细的情况 if (!__scrt_is_managed_app())exit(main_result);这里触发了一个断点很奇怪,这中断就发生了一次,代…

JDK 24:Java 24 中的新功能

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;历代文学&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;精通Java编程&#xff0c;高并发设计&#xf…

联想按下“AI加速键”!目标:与5000万中小企业共创

根据相关数据显示&#xff0c;截至2023年末中国中小企业数量超过5300万家&#xff0c;中小企业支撑了中国经济的发展与前进。在AI大模型风潮到来之际&#xff0c;相比于AI带给大企业的长期价值&#xff0c;AI对中小企业有着更加直接、显著、决定性的意义。同时&#xff0c;AI与…

【React】二、状态变量useState

文章目录 1、React中的事件绑定1.1 基础事件绑定1.2 使用事件对象参数1.3 传递自定义参数1.4 同时传递事件对象和自定义参数 2、React中的组件3、useState 1、React中的事件绑定 1.1 基础事件绑定 语法&#xff1a;on 事件名称 { 事件处理程序 }&#xff0c;整体上遵循驼峰…

计算机网络-IPSec VPN基本概念

企业分支之间经常有互联的需求&#xff0c;企业互联的方式很多&#xff0c;可以使用专线线路或者Internet线路。部分企业从成本和需求出发会选择使用Internet线路进行互联&#xff0c;但是使用Internet线路存在安全风险&#xff0c;如何保障数据在传输时不会被窃取&#xff1f;…

VirtualBox注册已有虚拟机:未能打开位于虚拟电脑E_INVALIDARG (0X80070057)

错误如下 解决办法1 产生虚拟机的机器&#xff0c;与当前使用机器不兼容。建议在当前机器重新产生虚拟机。比如我家里电脑是WIN7&#xff0c;公司电脑是WIN11。 原来的虚拟机内容&#xff0c;找老机器导出。 解决办法2&#xff08;存疑&#xff09; 搜索到一个说法&#xf…

浅谈网络 | 应用层之流媒体与P2P协议

目录 流媒体名词系列视频的本质视频压缩编码过程如何在直播中看到帅哥美女&#xff1f;RTMP 协议 P2PP2P 文件下载种子文件 (.torrent)去中心化网络&#xff08;DHT&#xff09;哈希值与 DHT 网络DHT 网络是如何查找 流媒体 直播系统组成与协议 近几年直播比较火&#xff0c;…

2023年第十四届蓝桥杯Scratch国赛真题—推箱子

推箱子 程序演示及其源码解析&#xff0c;可前往&#xff1a; https://www.hixinao.com/scratch/creation/show-188.html 若需在线编程&#xff0c;在线测评模考&#xff0c;助力赛事可自行前往题库中心&#xff0c;按需查找&#xff1a; https://www.hixinao.com/ 题库涵盖…

【学习总结|DAY012】Java面向对象基础

一、前言 今天主要学习了以下内容&#xff1a;面向对象的理解与使用、对象的内存布局、构造器的概念和作用、封装的重要性以及JavaBean实体类的实现等。下面我将详细阐述这些知识点。 二、面向对象的理解与使用 1. 什么是面向对象&#xff1f; 类&#xff1a;一种特殊的数据…

网络安全知识:网络安全网格架构

在数字化转型的主导下&#xff0c;大多数组织利用多云或混合环境&#xff0c;包括本地基础设施、云服务和应用程序以及第三方实体&#xff0c;以及在网络中运行的用户和设备身份。在这种情况下&#xff0c;保护组织资产免受威胁涉及实现一个统一的框架&#xff0c;该框架根据组…

Spring04——注解开发

Spring3.0启用了纯注解开发模式&#xff0c;使用Java类替代配置文件&#xff0c;开启了Spring快速开发赛道 Java类代替Spring核心配置文件&#xff0c; 配置类&#xff08;Configuration&#xff09; Configuration注解用于设定当前类为配置类ComponentScan注解用于设定扫描路…