关注微信公众号:【快乐程序猿】查看更多篇章
相信很多读者都知道多线程是什么,那Ring Buffer可能就不太清楚了,那我们先来介绍下什么是Ring Buffer。
Ring Buffer,也称为循环缓冲区,是一种固定大小的缓冲区,用于在生产者和消费者之间传递数据。它是一种数据结构,常用于需要缓冲数据流的场合,如音频处理、数据通信等。
缓冲区大小: Ring Buffer有一个固定的容量,即可以容纳的数据项数量。通常,该容量为2的幂,这样方便用位运算来做索引。
缓冲区指针: 它有两个主要的指针:
写指针(write pointer): 指向下一个要写入数据的位置。
读指针(read pointer): 指向下一个要读取数据的位置。
Ring Buffer 的核心概念是“循环”,即当写指针或读指针达到缓冲区的末尾时,它们会自动返回到缓冲区的起始位置。这种行为使得缓冲区可以不断地重复使用空间。
写入操作: 将数据写入写指针指向的位置,然后移动写指针。如果写指针在写入后达到了缓冲区末尾,它将跳转回起始位置。为了避免覆盖未读取的数据,一般在写入数据前需要检查缓冲区是否已满。
读取操作: 从读指针指向的位置读取数据,然后移动读指针。同样,如果读指针在读取后达到了缓冲区末尾,它将跳转回起始位置。
缓冲区为空: 当读指针和写指针指向同一个位置时,且没有写入任何新数据,这时缓冲区为空。
缓冲区为满: 当写指针的位置即将覆盖读指针的位置时,缓冲区为满。这通常是通过在缓冲区中保留一个空槽来判断的。
效率高: Ring Buffer 不需要动态分配内存,因为它的大小是固定的。
简单: 实现简单,只需要两个指针和一些边界检查。
音频和视频流处理
网络通信缓冲
日志系统中的缓冲
下面我们用一个示例来讲解音频场景下多线程如何写入和读取Ring Buffer,首先我们写一段获取音频数据的代码。
int audiorecordmic(void *arg)
{
int i, err, size;
char *buffer;
snd_pcm_t *capture_handle;
snd_pcm_hw_params_t *hw_params;
/*PCM的采样格式*/
snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE; // 采样位数:16bit、LE格式
if ((err = snd_pcm_open(&capture_handle, "capture_mic", SND_PCM_STREAM_CAPTURE, 0)) < 0)
{
exit(1);
}
if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0)
{
exit(1);
}
if ((err = snd_pcm_hw_params_any(capture_handle, hw_params)) < 0)
{
exit(1);
}
if ((err = snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0)
{
exit(1);
}
if ((err = snd_pcm_hw_params_set_format(capture_handle, hw_params, format)) < 0)
{
exit(1);
}
if ((err = snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &rate, 0)) < 0)
{
exit(1);
}
if ((err = snd_pcm_hw_params_set_channels(capture_handle, hw_params, 1)) < 0)
{
exit(1);
}
if ((err = snd_pcm_hw_params(capture_handle, hw_params)) < 0)
{
exit(1);
}
snd_pcm_hw_params_free(hw_params);
if ((err = snd_pcm_prepare(capture_handle)) < 0)
{
exit(1);
}
/*配置一个数据缓冲区用来缓冲数据*/
size = getdatasize();
buffer = malloc(size);
/*开始采集音频pcm数据*/
while (1)
{
// pthread_mutex_lock(&datamic.mutex);
/*从声卡设备读取一帧音频数据*/
if ((err = snd_pcm_readi(capture_handle, buffer, BUFFER_FRAME)) != BUFFER_FRAME)
{
printf("从音频接口读取失败! function = %s LINE = %d bufferlen = %d\n", __FUNCTION__, __LINE__, err);
exit(1);
}
printf("function = %s LINE = %d err = %d\n", __FUNCTION__, __LINE__, err);
write_micring_buffer(&datamic, buffer, size);
/*写数据到文件*/
// fwrite(datamic.buffer,(BUFFER_FRAME*2),sizeof(short),pcm_data_file_mic);
// sendto(g_socket, micbuffer, err * 2 * 2, 0, (struct sockaddr *)&client_address, client_len);
}
free(buffer);
snd_pcm_close(capture_handle);
return 0;
}
在上面代码中其实我们只需要关注snd_pcm_readi和write_micring_buffer这两个方法,snd_pcm_readi就是从驱动获取音频数据的接口函数,这里参数中包含了三个参数
capture_handle:PCM 设备句柄,它是通过 snd_pcm_open
打开设备时获得的,我们代码中也有,获得后下面也通过多个接口函数设置了一些参数,比如采样率、通道数、位深等等。
buffer:指向用于存储读取到的数据的缓冲区。数据是按照交错格式存储的,即多个通道的数据交替存储。
BUFFER_FRAME:想要读取的帧数。每帧包含所有通道的一个采样数据,这里我们定义的是2048。
write_micring_buffer这个方法就是我们将音频数据写入ringbuf的方法了,我们来重点理解下
void write_micring_buffer(shared_data_t *rb, const char *data, int length)
{
pthread_mutex_lock(&rb->mutex);
for (int i = 0; i < length; ++i)
{
rb->buffer[rb->head] = data[i];
rb->head = (rb->head + 1) % rb->size;
if (rb->head == rb->tail)
{
rb->tail = (rb->tail + 1) % rb->size; // Overwrite
}
}
pthread_cond_signal(&rb->cond);
pthread_mutex_unlock(&rb->mutex);
}
这里的锁是为了防止存取顺序出现问题,就是说在没有数据存入的时候不让他进行读取,如果代码逻辑写的比较完善的话,我个人理解按照ringbuffer的特性来说是不需要加锁的。这里后期我会尝试优化。
下面是一个for循环,判断条件就是我们传进来的length,这里的length对于音频数据buffer来说就是他的长度,我们在上个代码中可以看到使用malloc给buffer分配了一个size长度的空间,这个size就是计算公式就是(帧数*位深/8*通道数)得来的。
循环内第一步就是将data数据写入到ringbuffer中,然后ringbuffer的头指针加1,也就是向前移动一个字节。然后判断头指针和尾指针的位置是否相同,如果相同的话就尾指针向前移动一位,因为如果头指针尾指针相同就说明,头指针移动一个周期追赶上了尾指针。
下面我们看看另一个读取线程怎么写
int audiosendbuffer(void *arg)
{
snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE;
char *micbuffer;
int buffersize = getdatasize();
micbuffer = malloc(buffersize);
while (1)
{
read_micring_buffer(&datamic, micbuffer, buffersize);
fwrite(micbuffer, (BUFFER_FRAME), sizeof(short), pcm_data_file_mic);
}
return 0;
}
可以看到读取线程很简单只调用了read_micring_buffer这个函数,然后将获取到的数据写入到pcm文件中,以便后面进行验证。
int read_micring_buffer(shared_data_t *rb, char *data, int length)
{
pthread_mutex_lock(&rb->mutex);
while (rb->head == rb->tail)
{
pthread_cond_wait(&rb->cond, &rb->mutex);
}
int count = 0;
while (rb->tail != rb->head && count < length)
{
data[count++] = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
}
pthread_mutex_unlock(&rb->mutex);
return count;
}
这里锁的作用与写入的方法一致,我们主要看下面的逻辑代码。
首先判断头指针和尾指针位置是否相同,如果相同,就等待条件锁被唤醒,因为读的时候相同代表着没有数据写入,所以需要等待。
然后定义了一个count用来记录读到的数据长度,之后判断 头指针尾指针不相同并且count小于数据长度的时候,将ringbuffer的数据取出,注意这里用了rb->tail,从尾指针的位置开始取,依次写入到data中,然后将尾指针位置向前移动一位。
可以看到原理其实很简单就是从头指针的位置往ringbuffer写入数据,指针后移,读的时候从尾指针开始读,指针后移,依次循环操作。这样就实现了多线程操作ringbuffer的功能。
当然如果读者有更好的思路和方法可以跟我一起探讨