首页 > 其他分享 >4.采用锁操作并支持等待功能的线程安全队列

4.采用锁操作并支持等待功能的线程安全队列

时间:2024-12-28 15:30:29浏览次数:7  
标签:std head 队列 pop tail 线程 unique 等待 ptr

分析

书接上文

修改push()似乎并不困难:在函数末尾加上对data_cond.notify_one()的调用即可,与代码清单1(第一篇文章)一样。事情其实没那么简单,我们之所以采用精细粒度的锁,目的是尽可能提高并发操作的数量。如果在notify_one()调用期间,互斥依然被锁住,形式与代码清单1一样,而等待通知的线程却在互斥解锁前觉醒,它就需要继续等待互斥解锁。矛盾的是,若在解锁互斥之后调用notify_one(),那么互斥已经可以再次获取,并且超前一步,等着接受通知的线程对其加锁(前提是其他线程没有抢先将其重新锁住)。这点改进看似细微,但对某些情况却有重要作用。

wait_and_pop()就复杂得多,因为我们需要确定在哪里等待、根据什么断言唤醒等待、需要锁住什么互斥等。等待唤醒的条件是“队列非空”,用代码表示为 head!=tail。
按这种写法,要求两个互斥head_mutex和 tail_mutex 都被锁住,我们分析代码清单3(第三篇文章)的时候就已经确定,只有在读取 tail 指针时才有必要锁住互斥tai_mutex,而比较运算无须保护,本例同理。若我们将断言设定为head!=get_tail(),则只需持有互斥 head_mutex,所以在调用 data_cond.wait()时,就可以重新锁住head_mutex。只要我们加入了等待的逻辑,这种实现就与 try_pop()一样。
对于try_pop()的另一个重载和对应的 wait_and_pop() 的重载,我们也要谨慎思考和设计。在代码清单3中,try_pop()函数的结果通过共享指针std::shared_ptr的实例返回,其指向目标由old_head间接从 pop_head() 取得。如果模仿代码清单1,将以上方法改为 try_pop()的第一个重载的模式,令函数接收名为value 的引用参数,再由拷贝赋值操作赋予它 old_head 的值,就可能会出现与异常有关的问题。根据这种改动,在拷贝赋值操作执行时,数据已经从队列中移除,且互斥已经解锁,剩下的全部动作就是将数据返回给调用者。但是,如果拷贝赋值操作抛出了异常(完全有可能),则该项数据丢失,因为它无法回到队列本来的位置上。
若队列模板在具现化时,模板参数采用了实际类型T,而该类型支持不抛出异常的移动赋值操作,或不抛出异常的交换操作,我们即可使用类型T。然而,我们还是更希望实现通用的解决方法,对任何类型T都有效。在上述场景中,我们需要在队列移除节点以前,将可能抛出异常的操作移动到锁的保护范围之内。也就是说,我们还需要pop_head()的另一个作用,在改动队列之前就获取其存储的值。
相比而言,empty(就很简单了:只需锁住互斥head_mutex,然后检查 head_get_tail()。

一、实现

接口

template<typename T>
class threadsafe_queue{
private:
	struct node{
		std::shared_ptr<T> data;
		std::unique_ptr<node> next;
	};
	std::mutex head_mutex;
	std::unique_ptr<node> head;
	std::mutex tail_mutex;
	node* tail;
	std::condition_variable data_cond;
public:
	threadsafe_queue():
	head(new node),tail(head.get())
	{}
	threadsafe_queue(const threadsafe_queue& other)=delete;
	threadsafe_queue& operator=(const threadsafe_queue& other)=delete;
	std::shared_ptr<T> try_pop();
	bool try_pop(T& value);
	std::shared_ptr<T>wait_and_pop();
	void wait_and_pop(T& value);
	void push(T new_value);
	bool empty();
];

压入新数据

template<typename T>{
	void threadsafe_queue<T>::push(T new_value)
	std::shared_ptr<T> new_data(
	std::make_shared<T>(std::move(new_value)));
	std::unique_ptr<node>p(new node);
	{
		std::lock_guard<std::mutex> tail_lock(tail_mutex);
		tail->data=new_data;
		node* const new_tail=p.get();
		tail->next=std::move(p);
		tail=new_tail;
	}
	data_cond.notify_one();
}

我们提过,复杂之处全在于pop()上,我们运用几个辅助函数简化操作。

template<typename T>
class threadsafe_queue{
private :
	node* get_tail()
	{
		std::lock_guard<std::mutex> tail_lock(tail mutex);
		return tail;
	}
	std::unique_ptr<node> pop_head()①
	{
		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();));
		return std::move(head_lock);③
	}
	std::unique_ptr<node> wait_pop_head()
	{
		std::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();
	}
public:
	std::shared ptr<T> wait_and_pop()
	{
		std::unique_ptr<node> const old_head=wait_pop_head();
		return old_head->data;
	}
	void wait_and_pop(T& value)
	{
		std::unique_ptr<node> const old_head=wait_pop_head(value);
	}
};

这里展示出 wait_and_pop()实现代码,它含有几个辅助函数,用以简化代码和减少重复,如 pop_head()①和 wait_for_data()②。前者移除头节点而改动队列,后者则等待数据加入空队列,以将其弹出。wait_for_data()特别值得注意,它在条件变量上等待,以 lambda 函数作为断言,并且向调用者返回锁的实例③。因为 wait pop_head()的两个重载都会改动队列数据,并且都依赖wait_for_data()函数,而后者将锁返回则保证了头节点弹出的全过程都持有同一个锁④⑤。这也为里的 pop_head()和ty_pop()使用。

template<typename T>
class threadsafe_queue
private:
	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:
	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!=nullptr;
	}
	bool empty()
	{
		std::lock_guard<std::mutex> head_lock(head_mutex);
		return(head.get()==get_tail());
	}
);

总结

标签:std,head,队列,pop,tail,线程,unique,等待,ptr
From: https://blog.csdn.net/suy123/article/details/144789128

相关文章

  • 记手动粗浅管理多线程的工具类
    首先新建一个线程池管理工具类ThreadPoolManagerUtilimportlombok.extern.slf4j.Slf4j;importjava.util.concurrent.ArrayBlockingQueue;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ThreadFactory;importjava.util.concurrent.ThreadP......
  • 【Java并发】线程池的原理和使用
    目录什么是线程池为什么要用线程池(线程池的优势)创建线程池(三大方法)方法一:newFixedThreadPool(int)案例特点方法二:newsSingleThreadExecutor()案例特点方法三:newCachedThreadPool()案例特点自定义线程池(七大参数)1.corePoolSize(核心线程数)2.maximumPoolSize(最大......
  • 代码随想录算法训练营第六十天|Bellman_ford队列优化法(SPFA)、bellman_ford之判断负
    前言打卡代码随想录算法训练营第49期第六十天(づ◕‿◕)づ首先十分推荐学算法的同学可以先了解一下代码随想录,可以在B站卡哥B站账号、代码随想录官方网站代码随想录了解,卡哥清晰易懂的算法教学让我直接果断关注,也十分有缘和第49期的训练营大家庭一起进步。Bellman_ford队......
  • 多任务-线程
    线程:轻量级的进程,栈区独立(8M),与同一进程中的其他线程共用进程的堆区,数据区,文本区。1.线程的创建  线程由所属的进程创建,进程为其分配独立的栈区空间,  堆区,数据区,文本区与其他线程和所在进程共享。2.线程调度  宏观并行,微观串行 3.线程的消亡  1.线程......
  • Java多线程处理文件详解与代码示例
    在Java编程中,文件处理是一项常见的任务。当需要处理大量文件或处理文件的时间较长时,单线程的处理方式可能会显得效率低下。为了提高文件处理的效率,我们可以使用多线程技术。本文将详细介绍如何使用Java多线程来处理文件,并提供一个详细的代码示例,该示例可以直接运行。一、多线......
  • Python数据结构之队列
    1、对列队列(Queue)是一种线性数据结构,遵循先进先出(FIFO)的原则。可以将队列想象成排队的场景,最先排队的人最先被服务。2、队列的特点先进先出(FIFO):队列遵循先进先出的原则,第一个进入队列的元素最先被移除。两个操作端:队列在队尾插入元素,在队首移除元素,两个操作端分别负责不同......
  • 什么是线程同步?
    线程同步是指在多个线程并发执行时,保证它们按照一定的顺序执行以达到正确的结果。常用的线程同步机制有以下几种:互斥锁:使用互斥锁(Mutex)可以保证在同一时间只有一个线程可以访问共享资源。当一个线程获得了互斥锁后,其他线程需要等待该线程释放锁才能继续访问共享资源。信号量:信......
  • 什么是线程互斥?
    线程互斥(ThreadMutualExclusion)是一种同步机制,用于确保在多线程环境中,同一时间只有一个线程可以访问特定的资源或代码段。线程互斥的主要目的是防止多个线程同时修改共享数据,从而避免数据不一致和竞态条件(RaceConditions)。什么是竞态条件(RaceConditions)?竞态条件是指程序的输......
  • java 多线程处理list集合数据的实例应用
    众所周知创建线程的三种方式:继承Thread,重写run方法实现Runnable接口,重新run方法实现Callable接口,重写call方法下面使用Callable,来说一下为什么使用1.Thread类和Runnable接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。不能声明抛出检查型异常则......
  • 多线程的实现原理
    多线程编程是一种允许在同一程序中同时执行多个线程的技术,以提高程序的性能和响应性。多线程的实现原理涉及操作系统、编程语言和编译器等多个层面。以下是对多线程实现原理的详细解释:多线程的基本概念线程(Thread):线程是程序执行的基本单元,是操作系统能够进行运算调度的最小单......