队列的讲解:C++队列的使用
一.队列的介绍:
队列是C/C++中最基础的数据结构之一,队列本质上是一种线性表。它遵循着先进先出(fifo)的特点,在队列中一般在队尾插入,队头出队。这就相当于排队一样,刚入队的人需要排在队尾(rear),每次出队的都是在队首(front)。在实际开发中队列发挥着巨大的作用,比方说多线程数据传输、缓存数据的存储、中间件的设计等等。
从上面这张图我们可以看到,队尾入队了三个元素分别是1,2,3。1号数据最早入队、2号数据第二入队、3号数据最后入队。出队的时候,1号最早出队(pop1)、2号排在1号数据后面(pop2)、3号最后出队(pop3)。所以使用队列的时候,我们可以保证数据的顺序不会出现乱序的错误。
二.队列的用处:
队列常用于在多线程数据传输、数据解耦、缓存数据等方面。由于在大型的项目开发中,往往有许多线程同时运作。此时,许多线程之间需要进行数据的传递,所以此时我们就需要通过队列作为一条桥梁把数据从一个线程送到另外一个线程里面(如下图一就是队列在两个线程之间的通信)。线程一把数据按照顺序把数据包存储到Queue上、线程二、三也按照顺序从队列拿到数据。
(图一)
除了线程之间通信之外,队列还常用于数据量缓存方面。比方说,在音视频解码的时候,音视频数据会大量传入解码端。假设此时没有一个缓冲的时间,解码端可能会因为处理速度的问题,导致解码视频的时候会出现花屏、卡顿等问题。所以,此时我们就需要用队列进行缓冲,使其传输速度降下来,那解码端的解码压力就会大大降下来,此时解码出来的画面质量就会高很多,具体的流程如图二。
(图二)
三.C++ STL队列的用法:
C++库已经提供了一套队列的api方便开发者进行开发,这样我们就不用重新再新造轮子去实现队列。下面我们就来看看我们用stl queue去实现队列:
3.1. queue的初始化:
#include <queue>
std::queue<object> object_queue;
初始化stl的queue,需要做两步。第一步要包含<queue>头文件,#include<queue>;
第二步声明queue,std::queue<object> object_queue。这里的<object>里面的object是任意类型的数据,也包括结构体的数据。
3.2.queue的操作api:
front():返回 queue 中第一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
back():返回 queue 中最后一个元素的引用。如果 queue 是常量,就返回一个常引用;如果 queue 为空,返回值是未定义的。
push(const T& obj):在 queue 的尾部添加一个元素的副本。这是通过调用底层容器的成员函数 push_back() 来完成的。
push(T&& obj):以移动的方式在 queue 的尾部添加元素。这是通过调用底层容器的具有右值引用参数的成员函数 push_back() 来完成的。
pop():删除 queue 中的第一个元素。
size():返回 queue 中元素的个数。
empty():如果 queue 中没有元素的话,返回 true。
emplace():用传给 emplace() 的参数调用 T 的构造函数,在 queue 的尾部生成对象。
swap(queue<T> &other_q):将当前 queue 中的元素和参数 queue 中的元素交换。它们需要包含相同类型的元素。也可以调用全局函数模板 swap() 来完成同样的操作。
3.3.queue的demo
上面这个是一个简单的stl queue操作,先入队6个元素(0-5)。然后再连续出队pop,这里总共出队了4次,此时元素0 1 2 3全部出队并删除,所以打印front的元素是4。
四.多线程队列的框图:
上图是同一个典型的多线程入队,出队的过程。这里需要创建两个线程,一个是入队线程、一个是出队线程。入队线程主要是通过push的api向Queue的队尾插入数据,插入数据的同时通过pthread_cond_broadcast通知出队线程取出数据。此时出队线程正在等待入队线程的唤醒(pthread_cond_wait),若收到唤醒通知则让队列数据出队。
五.Linux多线程的基本API:
1. pthread_mutex_lock:
int pthread_mutex_lock(pthread_mutex_t *mutex);
第一个传入参数:pthread_mutex_t结构体指针
功能:这个是互斥锁加锁功能,就是每次线程调用的时候都会把锁加上,使其保证访问数据的原子性,直到解锁为止。
2. pthread_mutex_unlock:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
第一个传入参数:pthread_mutex_t结构体指针
功能:这个是互斥锁解锁功能,就是每次线程访问完资源的时候都会把锁解锁。
3. pthread_cond_broadcast:
int pthread_cond_broadcast(pthread_cond_t *cond)
传入参数:pthread_cond_t的结构体指针
功能:唤醒所有正在pthread_cond_wait(线程等待)的线程
4. pthread_cond_wait:
int pthread_cond_wait (pthread_cond_t *__restrict __cond , pthread_mutex_t *__restrict __mutex)
第一个参数:pthread_cond_t的结构体指针
第二个参数:pthread_mutex_t结构体指针
功能:线程等待并挂起,若被唤醒了,则直接跳出挂起状态。
在多线程里面所有的操作都需要上锁,包括出队、入队,或者其他的业务操作都需要上锁,保证数据的安全性。
六.推流项目中视频队列的实现:
这张图是视频队列实现的过程,VIDEO_QUEUE是一个类。这个类里面,封装了添加视频队列(putVideoPacketQueue)、获取视频队列数据(getVideoPacketQueue)、获取视频队列长度(getVideoQueueSize)。
3.1. VIDEO_QUEUE构造器:
这里创建一个VIDEO_QUEUE的C++的构造器,C++构造器主要初始化了线程的量。包括:线程锁的初始化(pthread_mutex_init)、线程条件变量的初始化(pthread_cond_init)。
3.2. putVideoPacketQueue的讲解:
putVideoPacketQueue主要是video_data_packet_t入队的过程,入队前需要加锁pthread_mutex_lock。然后进行入队操作video_packet_queue.push(video_packet),入队完成之后再通知出队线程取出队列数据pthread_cond_broadcast,最后解锁pthread_mutex_unlock。
3.3. getVideoPacketQueue的讲解:
getVideoPacketQueue主要是video_data_packet_t入队的过程,入队前需要加锁pthread_mutex_lock。然后判断视频队列是否有数据(video_packet_queue.size()==0)。若没有数据,则用pthread_cond_wait去等待线程被唤醒。若队列有数据则唤醒的此线程,则直接从队列取出数据。这里取出数据分两步:第一步,先把队列移动到最前面video_packet_queue.front()。第二步,video_packet_queue.pop出队并删除数据。
3.4. getVideoQueueSize的讲解:
getVideoQueueSize主要是获取当前队列的长度,获取长度的步骤跟上面也差不多。
首先,pthread_mutex_lock加锁,然后通过count = video_packet_queue.size(),获取队列的数量。然后pthread_mutex_unlock解锁。