无锁队列实现及使用场景

这篇具有很好参考价值的文章主要介绍了无锁队列实现及使用场景。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

写在前面

在看无锁队列之前,我们先来看看看队列的操作。队列是一种非常重要的数据结构,其特性是先进先出(FIFO),符合流水线业务流程。在进程间通信、网络通信间经常采用队列做缓存,缓解数据处理压力。根据队列操作的场景可分为以下4种:
无锁队列实现及使用场景
无锁队列应用场景

如果你的业务,数据量不大,一秒只需要处理几百上千的数据,就没必要用无锁队列了。当需要处理的数据非常多,比如,每秒需要处理几十万条数据时,可以考虑用无锁队列。

一、有锁和无锁

实际上有锁和无锁,就是我们平时所说的乐观锁和悲观锁:

  1. 悲观锁:一种悲观的加锁策略,它认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全;
  2. 乐观锁:是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加锁,因此线程将不断执行,不需要停止。一旦碰到冲突,就重试当前操作直到没有冲突为止。

无锁通过(CAS Compare And Swap)技术来实现,CAS是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作,从⽽避免多线程同时改写某⼀数据时由于执⾏顺序不确定性以及中断的不可预知性产⽣的数据不⼀致问题。

bool CAS( int * pAddr, int nExpected, int nNew )
{
	 if ( *pAddr == nExpected ) {
		 *pAddr = nNew ;
		 return true ;
	 }
	 else{
	 	 return false ;
	 }
}

工作原理:将pAddr地址中的元素与nExpected比较,如果相等,则更新pAddr的值为nNew,并返回true;否则返回false。

二、无锁队列的优势

上面我们提到,在数据量小的时候直接采用加锁的方式,实现资源的排他性访问即可。但是加锁的缺点也很明显:

  1. CPU会将大量的时间用在锁的维护上,而不是数据处理;
  2. 在线程之间切换的时候,导致Cache中的数据失效。CPU访问Cache的速度是远大于内存的,所以需要尽量减少线程频繁切换 。

三、无锁队列实现

3.1 zmq实现

zmq实现无锁队列,只要在ypipe.hpp 和 yqueue.hpp 两个文件,适用于一读一写的场景。

3.1.1 yqueue.hpp
template <typename T, int N>
class yqueue_t
{
	......
	
private:
    //  Individual memory chunk to hold N elements.
    // 链表结点称之为chunk_t
    struct chunk_t
    {
        T values[N]; //每个chunk_t可以容纳N个T类型的元素,以后就以一个chunk_t为单位申请内存
        chunk_t *prev;
        chunk_t *next;
    };

    chunk_t *begin_chunk; // 链表头结点
    int begin_pos;        // 起始点
    chunk_t *back_chunk;  // 队列中最后一个元素所在的链表结点
    int back_pos;         // 尾部
    chunk_t *end_chunk;   // 拿来扩容的,总是指向链表的最后一个结点
    int end_pos;
    
    atomic_ptr_t<chunk_t> spare_chunk; //空闲块(把所有元素都已经出队的chunk,称为空闲块),读写线程的共享变量
};

yqueue_t 结构,内部由⼀个⼀个chunk组成,每个chunk保存N个元素。chunk之间通过指针连接。

无锁队列实现及使用场景

当队列空间不⾜时每次分配⼀个chunk_t,每个chunk_t能存储N个元素。
在数据出队列后,队列有多余空间的时候,回收的chunk也不是⻢上释放,⽽是根据局部性原理先回收到spare_chunk⾥⾯,当再次需要分配chunk_t的时候从spare_chunk中获取。

yqueue_t内部有三个chunk_t类型指针以及对应的索引位置:

  1. begin_chunk/begin_pos:begin_chunk⽤于指向队列头的chunk,begin_pos⽤于指向队列第⼀个元素在当前chunk中的位置。
  2. back_chunk/back_pos:back_chunk⽤于指向队列尾的chunk,back_po⽤于指向队列最后⼀个元素在当前chunk的位置。
  3. end_chunk/end_pos:由于chunk是批量分配的,所以end_chunk⽤于指向分配的最后⼀个chunk位置

这⾥特别需要注意区分back_chunk/back_pos和end_chunk/end_pos的作⽤:

  • back_chunk/back_pos:对应的是元素存储位置;
  • end_chunk/end_pos:决定是否要分配chunk或者回收chunk。
yqueue_t 构造函数
    inline yqueue_t()
    {
        begin_chunk = (chunk_t *)malloc(sizeof(chunk_t));
        alloc_assert(begin_chunk);
        begin_pos = 0;
        back_chunk = NULL; //back_chunk总是指向队列中最后一个元素所在的chunk,现在还没有元素,所以初始为空
        back_pos = 0;
        end_chunk = begin_chunk; //end_chunk总是指向链表的最后一个chunk
        end_pos = 0;
    }

说明:
采用chunk的机制,减少内存分配次数,采用spark_chunk的机制回收读完的chunk,充分利用局部性原理,提升性能。
end_chunk 总是指向最后分配的chunk,刚分配出来的chunk,end_pos也是0
back_chunk 在插入元素时才会指向对应的chunk,初始化的是是指向NULL的。
无锁队列实现及使用场景

ront、back函数

inline T &front() // 返回的是引⽤,是个左值,调⽤者可以通过其修改容器的值
{
	return begin_chunk->values[begin_pos]; // 队列⾸个chunk对应的的begin_pos
}

inline T &back() // 返回的是引⽤,是个左值,调⽤者可以通过其修改容器的值
{
	return back_chunk->values[back_pos];
}

说明:
我们可以通过 begin_chunk->values[begin_pos] 获取到队列的头部,以及back_chunk->values[back_pos]获取到队列的尾部

Push函数

每次调用push,会更新 back_chunk 和 back_pos ,以及根据end_pos的值,决定是否需要重新分配chunk。

inline void push()
    {
        back_chunk = end_chunk;
        back_pos = end_pos; //

        if (++end_pos != N) //end_pos!=N表明这个chunk节点还没有满
            return;

        chunk_t *sc = spare_chunk.xchg(NULL); // 为什么设置为NULL? 因为如果把之前值取出来了则没有spare chunk了,所以设置为NULL
        if (sc)                               // 如果有spare chunk则继续复用它
        {
            end_chunk->next = sc;
            sc->prev = end_chunk;
        }
        else // 没有则重新分配
        {
            // static int s_cout = 0;
            // printf("s_cout:%d\n", ++s_cout);
            end_chunk->next = (chunk_t *)malloc(sizeof(chunk_t)); // 分配一个chunk
            alloc_assert(end_chunk->next);
            end_chunk->next->prev = end_chunk;  
        }
        end_chunk = end_chunk->next;
        end_pos = 0;
    }

重新分配规则如下:

  1. 如果 ++end_pos != N 说明当前chunk还有空间,直接返回
  2. 如果++end_pos == N 说明,当前chunk只有N-1的位置可用,需要再按分配一个chunk。 这个chunk 会先尝试从spare_chunk获取,如果spare_chunk为NULL,则需要重新分配。
pop函数

pop的时候begin_pos就会++,也就是会更新front的位置。当chunk 中的所有元素都被取出才会触发chunk回收机制。spare_chunk的操作要求是原子操作,因为读写线程都会访问spare_chunk。

inline void pop()
    {
        if (++begin_pos == N) // 删除满一个chunk才回收chunk
        {
            chunk_t *o = begin_chunk;
            begin_chunk = begin_chunk->next;
            begin_chunk->prev = NULL;
            begin_pos = 0;

            //  'o' has been more recently used than spare_chunk,
            //  so for cache reasons we'll get rid of the spare and
            //  use 'o' as the spare.
            chunk_t *cs = spare_chunk.xchg(o); //由于局部性原理,总是保存最新的空闲块而释放先前的空闲快
            free(cs);
        }
    }
3.2 ypipe_t
    inline ypipe_t()
    {
        //  Insert terminator element into the queue.
        queue.push(); //yqueue_t的尾指针加1,开始back_chunk为空,现在back_chunk指向第一个chunk_t块的第一个位置

        //  Let all the pointers to point to the terminator.
        //  (unless pipe is dead, in which case c is set to NULL).
        r = w = f = &queue.back(); //就是让r、w、f、c四个指针都指向这个end迭代器
        c.set(&queue.back());
    }

在ypipe对象创建的时候,更新yqueue的back_chunk的值,以及初始化 r w f c四个指针。
无锁队列实现及使用场景

write函数

write的incomplete_标志位决定要不要更新f的位置

    inline void write(const T &value_, bool incomplete_)
    {
        //  Place the value to the queue, add new terminator element.
        queue.back() = value_;
        queue.push();

        //  Move the "flush up to here" poiter.
        if (!incomplete_)
        {
            f = &queue.back(); // 记录要刷新的位置
            // printf("1 f:%p, w:%p\n", f, w);
        }
        else
        {
            //  printf("0 f:%p, w:%p\n", f, w);
        }
    }
flush函数

flush的核心是更新c指针和w指针的位置

inline bool flush()
    {
        //  If there are no un-flushed items, do nothing.
        if (w == f) // 不需要刷新,即是还没有新元素加入
            return true;

        //  Try to set 'c' to 'f'.
        // read时如果没有数据可以读取则c的值会被置为NULL,如果c==null 说明read线程在 休眠,可以安全的设置c的值
        if (c.cas(w, f) != w) // 尝试将c设置为f,即是准备更新w的位置
        {

            //  Compare-and-swap was unseccessful because 'c' is NULL.
            //  This means that the reader is asleep. Therefore we don't
            //  care about thread-safeness and update c in non-atomic
            //  manner. We'll return false to let the caller know
            //  that reader is sleeping.
            c.set(f); // 更新w的位置
            w = f;
            return false; //线程看到flush返回false之后会发送一个消息给读线程,这需要写业务去做处理
        }
        else  // 读端还有数据可读取
        {
            //  Reader is alive. Nothing special to do now. Just move
            //  the 'first un-flushed item' pointer to 'f'.
            w = f;             // 只需要更新w的位置
            return true;
        }
    }
read函数

read会先采取预读的机制,判断有无数据可读,可读就填充到value_。核心就是 check_read函数。主要通过比较c和队头的位置判断是否有数据可读,如果可读返回的就是flush后的f的位置。

    inline bool check_read()
    {
        //  Was the value prefetched already? If so, return.
        if (&queue.front() != r && r) //判断是否在前几次调用read函数时已经预取数据了return true;
            return true;

        //  There's no prefetched value, so let us prefetch more values.
        //  Prefetching is to simply retrieve the
        //  pointer from c in atomic fashion. If there are no
        //  items to prefetch, set c to NULL (using compare-and-swap).
        // 两种情况
        // 1. 如果c值和queue.front()相等,返回旧c值,将c值置为NULL,说明此时没有数据可读
        //    当c==&queue.front()时,代表数据被取完了,这时把c指向NULL,接着读线程会睡眠,这也是给写线程检查读线程是否睡眠的标志。
        // 2. 如果c值和queue.front()不相等, 直接返回c值,此时可能有数据度的去
        r = c.cas(&queue.front(), NULL); //尝试预取数据

        //  If there are no elements prefetched, exit.
        //  During pipe's lifetime r should never be NULL, however,
        //  it can happen during pipe shutdown when items are being deallocated.
        if (&queue.front() == r || !r) //判断是否成功预取数据
            return false;

        //  There was at least one value prefetched.
        return true;
    }

    inline bool read(T *value_)
    {
        //  Try to prefetch a value.
        if (!check_read())
            return false;

        //  There was at least one value prefetched.
        //  Return it to the caller.
        *value_ = queue.front();
        queue.pop();
        return true;
    }
小结

flush 会更新c 和 w 指针的位置为当前已经写入成功的位置。 read操作会通过比较c 和 front的关系,如果有就预读一部分元素,直至全部读完。在没有数据可读的时候,读会休眠。 注意c指针,读写线程都会访问到,所以需要CAS操作保证线程安全。

3.2 循环数组完整代码

ArrayLockFreeQueue.h

#ifndef _ARRAYLOCKFREEQUEUE_H___
#define _ARRAYLOCKFREEQUEUE_H___

#include <stdint.h>

#ifdef _WIN64
#define QUEUE_INT int64_t
#else
#define QUEUE_INT unsigned long
#endif

#define ARRAY_LOCK_FREE_Q_DEFAULT_SIZE 65535 // 2^16

template <typename ELEM_T, QUEUE_INT Q_SIZE = ARRAY_LOCK_FREE_Q_DEFAULT_SIZE>
class ArrayLockFreeQueue
{
public:

	ArrayLockFreeQueue();
	virtual ~ArrayLockFreeQueue();

	QUEUE_INT size();  // 队列大小

	bool enqueue(const ELEM_T &a_data);    // 入队列
	bool dequeue(ELEM_T &a_data);          // 出队列
    bool try_dequeue(ELEM_T &a_data);      // 尝试入队列

private:

	ELEM_T m_thequeue[Q_SIZE];

	volatile QUEUE_INT m_count;
	volatile QUEUE_INT m_writeIndex;

	volatile QUEUE_INT m_readIndex;

	volatile QUEUE_INT m_maximumReadIndex;

	inline QUEUE_INT countToIndex(QUEUE_INT a_count);
};


#include "ArrayLockFreeQueueImp.h"

#endif

三种不同下标的含义:

  • m_writeIndex;//新元素⼊列时存放位置在数组中的下标
  • m_readIndex;/ 下⼀个出列的元素在数组中的下标
  • m_maximumReadIndex; //最后⼀个已经完成⼊列操作的元素在数组中的下标。如果它的值跟m_writeIndex不⼀致,表明有写请求尚未完成。这意味着,有写请求成功申请了空间但数据还没完全写进队列。所以如果有线程要读取,必须要等到写线程将数据完全写⼊到队列之后。

以上3种下标都是必须的,因为队列允许任意数量的⽣产者和消费者围绕着它⼯作。已经存在⼀种基于循环数组的⽆锁队列,使得唯⼀的⽣产者和唯⼀的消费者可以良好的⼯作[11]。它的实现相当简洁⾮常值得阅读。

该程序使⽤gcc内置的__sync_bool_compare_and_swap,但重新做了宏定义封装。

#define CAS(a_ptr, a_oldVal, a_newVal) __sync_bool_compare_and_swap(a_ptr, a_oldVal, a_newVal)

ArrayLockFreeQueueImp.h

#ifndef _ARRAYLOCKFREEQUEUEIMP_H___
#define _ARRAYLOCKFREEQUEUEIMP_H___
#include "ArrayLockFreeQueue.h"

#include <assert.h>
#include "atom_opt.h"

template <typename ELEM_T, QUEUE_INT Q_SIZE>
ArrayLockFreeQueue<ELEM_T, Q_SIZE>::ArrayLockFreeQueue() :
	m_writeIndex(0),
	m_readIndex(0),
	m_maximumReadIndex(0)
{
	m_count = 0;
}

template <typename ELEM_T, QUEUE_INT Q_SIZE>
ArrayLockFreeQueue<ELEM_T, Q_SIZE>::~ArrayLockFreeQueue()
{

}

template <typename ELEM_T, QUEUE_INT Q_SIZE>
inline QUEUE_INT ArrayLockFreeQueue<ELEM_T, Q_SIZE>::countToIndex(QUEUE_INT a_count)
{
	return (a_count % Q_SIZE);		// 取余的时候
}

template <typename ELEM_T, QUEUE_INT Q_SIZE>
QUEUE_INT ArrayLockFreeQueue<ELEM_T, Q_SIZE>::size()
{
	QUEUE_INT currentWriteIndex = m_writeIndex;
	QUEUE_INT currentReadIndex = m_readIndex;

	if(currentWriteIndex>=currentReadIndex)
		return currentWriteIndex - currentReadIndex;
	else
		return Q_SIZE + currentWriteIndex - currentReadIndex;

}

template <typename ELEM_T, QUEUE_INT Q_SIZE>
bool ArrayLockFreeQueue<ELEM_T, Q_SIZE>::enqueue(const ELEM_T &a_data)
{
	QUEUE_INT currentWriteIndex;		// 获取写指针的位置
	QUEUE_INT currentReadIndex;
	// 1. 获取可写入的位置
	do
	{
		currentWriteIndex = m_writeIndex;
		currentReadIndex = m_readIndex;
		if(countToIndex(currentWriteIndex + 1) ==
			countToIndex(currentReadIndex))
		{
			return false;	// 队列已经满了	
		}
		// 目的是为了获取一个能写入的位置
	} while(!CAS(&m_writeIndex, currentWriteIndex, (currentWriteIndex+1)));
	// 获取写入位置后 currentWriteIndex 是一个临时变量,保存我们写入的位置
	// We know now that this index is reserved for us. Use it to save the data
	m_thequeue[countToIndex(currentWriteIndex)] = a_data;  // 把数据更新到对应的位置

	// 2. 更新可读的位置,按着m_maximumReadIndex+1的操作
 	// update the maximum read index after saving the data. It wouldn't fail if there is only one thread 
	// inserting in the queue. It might fail if there are more than 1 producer threads because this
	// operation has to be done in the same order as the previous CAS
	while(!CAS(&m_maximumReadIndex, currentWriteIndex, (currentWriteIndex + 1)))
	{
		 // this is a good place to yield the thread in case there are more
		// software threads than hardware processors and you have more
		// than 1 producer thread
		// have a look at sched_yield (POSIX.1b)
		sched_yield();		// 当线程超过cpu核数的时候如果不让出cpu导致一直循环在此。
	}

	AtomicAdd(&m_count, 1);

	return true;

}

template <typename ELEM_T, QUEUE_INT Q_SIZE>
bool ArrayLockFreeQueue<ELEM_T, Q_SIZE>::try_dequeue(ELEM_T &a_data)
{
    return dequeue(a_data);
}

template <typename ELEM_T, QUEUE_INT Q_SIZE>
bool ArrayLockFreeQueue<ELEM_T, Q_SIZE>::dequeue(ELEM_T &a_data)
{
	QUEUE_INT currentMaximumReadIndex;
	QUEUE_INT currentReadIndex;

	do
	{
		 // to ensure thread-safety when there is more than 1 producer thread
       	// a second index is defined (m_maximumReadIndex)
		currentReadIndex = m_readIndex;
		currentMaximumReadIndex = m_maximumReadIndex;

		if(countToIndex(currentReadIndex) ==
			countToIndex(currentMaximumReadIndex))		// 如果不为空,获取到读索引的位置
		{
			// the queue is empty or
			// a producer thread has allocate space in the queue but is 
			// waiting to commit the data into it
			return false;
		}
		// retrieve the data from the queue
		a_data = m_thequeue[countToIndex(currentReadIndex)]; // 从临时位置读取的

		// try to perfrom now the CAS operation on the read index. If we succeed
		// a_data already contains what m_readIndex pointed to before we 
		// increased it
		if(CAS(&m_readIndex, currentReadIndex, (currentReadIndex + 1)))
		{
			AtomicSub(&m_count, 1);	// 真正读取到了数据,元素-1
			return true;
		}
	} while(true);

	assert(0);
	 // Add this return statement to avoid compiler warnings
	return false;

}
#endif

运行原理:

一读一写

  1. 写数据分为两步(1)分配空间;(2)拷贝数据。当生产者向队列中插入数据时,会先通过WriteIdex来申请空间。MaximumReadIndex指向最后⼀个存放有效数据的位置(也就是实际的队列尾)。一旦空间申请完成,生产者就可以拷贝数据到该位置,然后MaximumReadIndex+1与WriteIdex保持一致。
  2. 读数据:消费者线程从ReadIndex的位置取数据,然后尝试用CAS将ReadIndex加一,把该元素会被pop出队列
    无锁队列实现及使用场景

多读多写

  1. 写数据:多线程与单线程写的区别主要在,可能统一时刻有多个线程往队列中写数据,假设当前有两个线程A、B:A线程申请空间成功WriteIdex+1,还未拷贝数据,线程B又申请了空间WriteIdex+1,此时,WriteIdex已经往后移动了两格。对MaximumReadIndex的递增操作必须严格遵循⼀个顺序:第⼀个⽣产者线程⾸先递增MaximumReadIndex,接着才轮到第⼆个⽣产者。这个顺序必须被严格遵守的原因是,我们必须保证数据被完全拷⻉到队列之后才允许消费者线程将其出列。
  2. 读数据:消费者线程从ReadIndex的位置取数据,然后尝试用CAS将ReadIndex加一。如果操作成功,元素出队。因为ReadIndex的更新是原子的,所有同一时刻只有一个线程能读到数据,并更新ReadIndex的值。

无锁队列实现及使用场景
无锁队列实现及使用场景
无锁队列实现及使用场景

特殊情况:
现在有⼀个⽣产者正在向队列中添加元素。它已经成功的申请了空间,但尚未完成数据拷⻉。任何其它企图从队列中移除元素的消费者都会发现队列⾮空(因为writeIndex不等于readIndex)。但它不能读取readIndex所指向位置中的数据,因为readIndex与MaximumReadIndex相等。消费者将会在do循环中不断的反复尝试,直到⽣产者完成数据拷⻉增加MaximumReadIndex的值,或者队列变成空(这在多个消费者的场景下会发⽣)
无锁队列实现及使用场景

无锁队列实现及使用场景

推荐一个零声学院免费教程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:文章来源地址https://www.toymoban.com/news/detail-439387.html

到了这里,关于无锁队列实现及使用场景的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深入理解数据结构:队列的实现及其应用场景

    队列(Queue)是一种具有先进先出(FIFO)特性的数据结构。在队列中,数据的插入和删除操作分别在队列的两端进行。插入操作在队列的尾部进行,而删除操作则在队列的头部进行。这种特性使得队列在很多实际应用中非常有用,比如任务调度、缓冲区管理等。 线性表是一种

    2024年04月28日
    浏览(53)
  • python消息队列4种方法及使用场景

    Python 有许多消息队列实现,其中一些最流行的包括: 一:RabbitMQ 是一个高度可靠的消息队列系统,用于发送和接收消息,支持多种消息协议。一个开源的消息队列系统,具有高可用性、高可靠性和高可扩展性等特点,适用于以下场景: 异步任务处理:当应用需要异步执行任

    2024年02月03日
    浏览(53)
  • ES实现“小于XX时间”排前面(或后面)“大于XX时间”排后面(或前面)排序

    一位学生问我一个问题,实现es查询:对于查询的结果要分成两类【过期和没过期,按照过期时间判断】。没过期的排在前面,过期的排在后面。最后,不管是过期的还是没过期的,在组内都再按照标定时间字段进行倒排序。 是否过期通过 过期时间字段 进行判断。 对于此类

    2024年02月15日
    浏览(47)
  • Unity实现模型显示在UI前面

    1.先创建一个Cube充当人物模型 2.创建一个血条,这边血条用Scrollbar实现,用Scrollbar实现血条的话,需要将里面的参数都清空 血条颜色也通过这边设置   然后再把这个size拉满就可以实现血条效果了,如果要实现掉血效果,直接通过代码调用到这个size进行减少便可以了   3.回归

    2024年02月05日
    浏览(44)
  • 消息队列测试场景和redis测试场景

    解题思路: 什么是消息队列。 消息队列应用场景。 消息队列测试点列举。 Broker:消息服务器,提供消息核心服务 Producer:消息生产者,业务的发起方,负责生产消息传输给 broker。 Consumer:消息消费者,业务的处理方,负责从 broker 获取消息并进行业务逻辑处理。 异步通信

    2024年02月04日
    浏览(45)
  • 关于Spark基本问题及结构[月薪2w的人都在看]

    目录   1.Spark是什么? 2.Spark与Hadoop  Spark与MapReduce对比 Spark与Hadoop 优点                          3. 什么是结构化数据? 什么是非结构化数据? 什么是结构化数据? 什么是非结构化数据? 4.Spark 架构作业执行原理 了解Spark架构 客户端 Driver  SparkContext  Cluster Manager SparkWorker

    2024年03月14日
    浏览(57)
  • [ RabbitMQ 消息队列来处理高并发场景 ]

    目录 首先,需要创建一个 RabbitMQ 的连接和消息通道。 然后,需要创建一个生产者来发送消息到消息队列。 最后,需要创建一个消费者来消费消息队列中的消息。 RabbitMQ 消息队列可以提高代码执行性能,主要体现在以下几个方面:  RabbitMQ 实现保持消息一致性的demo 在上面的

    2024年02月16日
    浏览(50)
  • 说说你对栈、队列的理解?应用场景?

    栈(stack)又名堆栈,它是一种运算受限的线性表,限定仅在表尾进行插入和删除操作的线性表 表尾这一端被称为栈顶,相反地另一端被称为栈底,向栈顶插入元素被称为进栈、入栈、压栈,从栈顶删除元素又称作出栈 所以其按照先进后出的原则存储数据,先进入的数据被压

    2024年04月11日
    浏览(49)
  • 【数据结构】队列的使用|模拟实现|循环队列|双端队列|面试题

    1.1 概念 队列 :只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为 队尾(Tail/Rear) 出队列:进行删除操作的一端称为 队头(Head/Front) 队列和栈的区别: 队列是 先进先出(队

    2024年02月03日
    浏览(45)
  • Unity如何实现让Sprite和UI显示在模型前面而不会被模型遮挡

    在不使用Shader的情况下实现UI显示在模型前面方法 ps:本人只做记笔记使用 使用前: 使用后: 1.首先需要把显示的UI或者Sprite的层级设置为UI层或者你自定义的层 2.创建俩个摄像机 3.主摄像机不变,需要渲染的层级依旧是Everything 次摄像机的CullingMask只选择UI或者自定义层,以

    2024年02月22日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包