C 언어로 구현하는 원형 큐 (Circular Queue) 자료구조

원형 버퍼(环形缓冲区)는 전형적인 생산자-소비자(Producer-Consumer) 모델을 구현하는 자료구조입니다. 비유하자면, 생산자가 계속 물을 채우고 소비자가 물을 빼는 웅덩이와 같습니다.

여기서 의문이 생길 수 있습니다. "생산자와 소비자 사이에 왜 굳이 버퍼 메모리 공간을 두는가? 생산자의 파이프 끝을 소비자의 파이프 시작 부분에 직접 연결하면 공간을 절약할 수 있지 않은가?"

이 방식은 작동하지 않습니다. 생산자가 물을 생산하는 속도와 소비자가 물을 소비하는 속도를 알 수 없기 때문입니다. 두 속도가 강제로 맞춰지면 속도 차이로 인해 파이프가 터질 위험이 있습니다.

오디오 시스템 프레임워크(예: ALSA)는 원형 큐를 사용하며, 생산자와 소비자 속도가 맞지 않을 때 xrun 문제가 발생합니다.


원형 큐의 특징

1. 배열 기반 원형 버퍼

배열로 원형 버퍼를 구성하려면 다음 세 가지 요소가 필요합니다:

  • 읽기 위치(R)
  • 쓰기 위치(W)
  • 버퍼 길이(Len)

초기 상태에서 R과 W는 배열의 시작 주소(인덱스 0)를 가리킵니다.

  • 버퍼가 비었는지 확인: R == W 이면 비어 있음
  • 버퍼가 가득 찼는지 확인: (W - R) == Len 이면 가득 참

2. 데이터 3개 쓰기

데이터 3개를 쓰면 W = 3, R = 0 이 됩니다.

3. 데이터 2개 읽기

데이터 2개를 읽으면 R = 2, W = 3 (변화 없음) 입니다.

4. 데이터 3개 추가 쓰기

W가 Len을 초과하면, 실제 데이터를 넣을 위치는 W % Len 연산으로 구합니다. 예를 들어, Len=5일 때 W=6이면 6 % 5 = 1 인덱스에 데이터를 저장합니다.

5. 데이터 1개 더 쓰기 (버퍼 가득 참)

이 시점에서 (W - R) == Len 이므로 버퍼가 가득 차서 더 이상 쓸 수 없습니다.


C 언어 코드 구현

다음은 가장 기본적인 원형 큐 구현 예제입니다.

#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 10

/* 원형 큐 구조체 */
typedef struct {
    int data[BUFFER_SIZE];
    int write_pos;
    int read_pos;
} CircularQueue;

/* 큐 초기화 */
CircularQueue* queue_init(void) {
    CircularQueue *q = (CircularQueue*)malloc(sizeof(CircularQueue));
    if (q == NULL) {
        printf("메모리 할당 실패\n");
        return NULL;
    }
    q->write_pos = 0;
    q->read_pos = 0;
    return q;
}

/* 큐가 가득 찼는지 확인 */
int is_full(CircularQueue *q) {
    return ((q->write_pos - q->read_pos) == BUFFER_SIZE);
}

/* 큐가 비었는지 확인 */
int is_empty(CircularQueue *q) {
    return (q->write_pos == q->read_pos);
}

/* 데이터 추가 (enqueue) */
int enqueue(CircularQueue *q, int value) {
    if (q == NULL) return -1;
    if (is_full(q)) {
        printf("큐가 가득 참\n");
        return -2;
    }
    q->data[q->write_pos % BUFFER_SIZE] = value;
    q->write_pos++;
    return 0;
}

/* 데이터 추출 (dequeue) */
int dequeue(CircularQueue *q) {
    if (q == NULL) return -1;
    if (is_empty(q)) {
        printf("큐가 비어 있음\n");
        return -2;
    }
    int value = q->data[q->read_pos % BUFFER_SIZE];
    q->read_pos++;
    return value;
}

/* 큐 메모리 해제 */
void queue_destroy(CircularQueue *q) {
    if (q == NULL) return;
    free(q);
}

int main() {
    CircularQueue *q = queue_init();
    
    /* 데이터 10개 쓰기 */
    for (int i = 0; i < 10; i++) {
        enqueue(q, i);
    }
    
    /* 데이터 10개 읽기 */
    for (int i = 0; i < 10; i++) {
        printf("%d ", dequeue(q));
    }
    printf("\n");
    
    queue_destroy(q);
    return 0;
}

최적화된 버전 (비트 연산 활용)

버퍼 크기가 2의 거듭제곱일 때, 모듈로 연산(%) 대신 비트 AND(&) 연산을 사용할 수 있습니다. 이는 연산 속도를 높입니다.

#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 64  /* 2의 거듭제곱 */
#define INDEX_MASK (BUFFER_SIZE - 1)

typedef struct {
    int data[BUFFER_SIZE];
    int write_pos;
    int read_pos;
} FastQueue;

FastQueue* fast_queue_init(void) {
    FastQueue *q = (FastQueue*)malloc(sizeof(FastQueue));
    if (q == NULL) {
        printf("메모리 할당 실패\n");
        return NULL;
    }
    q->write_pos = 0;
    q->read_pos = 0;
    return q;
}

int fast_is_full(FastQueue *q) {
    return ((q->write_pos - q->read_pos) == BUFFER_SIZE);
}

int fast_is_empty(FastQueue *q) {
    return (q->write_pos == q->read_pos);
}

int fast_enqueue(FastQueue *q, int value) {
    if (q == NULL) return -1;
    if (fast_is_full(q)) {
        printf("큐가 가득 참\n");
        return -2;
    }
    q->data[q->write_pos & INDEX_MASK] = value;
    q->write_pos++;
    return 0;
}

int fast_dequeue(FastQueue *q) {
    if (q == NULL) return -1;
    if (fast_is_empty(q)) {
        printf("큐가 비어 있음\n");
        return -2;
    }
    int value = q->data[q->read_pos & INDEX_MASK];
    q->read_pos++;
    return value;
}

void fast_queue_destroy(FastQueue *q) {
    if (q == NULL) return;
    free(q);
}

요약

원형 큐는 오디오 데이터 처리, 네트워크 패킷 버퍼링, 임베디드 시스템 등 다양한 분야에서 널리 사용됩니다. 위 예제는 기본적인 동작 원리를 설명하며, 실제 프로젝트에서는 더 복잡한 조건(멀티스레드 안전성, 동적 크기 조정 등)을 고려해야 합니다. 이 기본 구조를 이해하면 실제 개발에 응용할 수 있습니다.

태그: C 언어 원형 큐 원형 버퍼 Circular Queue 자료구조

6월 11일 20:45에 게시됨