C 언어로 구현하는 뮤텍스 기반 캐시 시스템: 개념부터 실습까지

뮤텍스를 활용한 캐시 시스템 구현은 멀티스레드 환경에서 데이터의 동기화와 효율성을 확보하는 데 필수적인 기술입니다. 본 기사에서는 뮤텍스의 기본 개념부터 C 언어로 구현하는 구체적인 방법, 성능 테스트 및 실전 예제까지 다룹니다. 이를 통해 멀티스레드 환경에서 안정적이고 효율적인 캐시 시스템을 설계하고 구현하는 데 필요한 지식을 습득할 수 있습니다.

목차

멀티스레딩과 뮤텍스의 기본 개념


멀티스레딩은 하나의 프로그램이 여러 작업을 동시에 처리할 수 있게 해주는 프로그래밍 기법입니다. 그러나 여러 스레드가 공유 자원에 동시에 접근할 경우 데이터 손상이나 충돌이 발생할 수 있습니다.

뮤텍스란 무엇인가


뮤텍스(Mutex, Mutual Exclusion)는 이러한 문제를 해결하기 위해 사용되는 동기화 도구로, 한 번에 하나의 스레드만 공유 자원에 접근하도록 제한합니다. 뮤텍스는 “잠금”과 “해제”라는 두 가지 주요 작업을 통해 작동합니다:

  • 잠금(Lock): 공유 자원에 접근하려는 스레드는 먼저 뮤텍스를 잠급니다.
  • 해제(Unlock): 작업이 끝난 후 공유 자원에 대한 잠금을 해제하여 다른 스레드가 접근할 수 있도록 합니다.

뮤텍스와 동기화 문제 해결


멀티스레드 환경에서의 주요 문제는 데이터 경합과 교착 상태입니다. 뮤텍스는 다음과 같은 방식으로 이를 해결합니다:

  1. 데이터 경합 방지: 동시에 여러 스레드가 자원에 접근하지 못하도록 제어합니다.
  2. 교착 상태 방지: 적절한 설계를 통해 스레드 간 무한 대기 상황을 방지합니다.

뮤텍스의 실제 사용 사례


예를 들어, 은행 애플리케이션에서 계좌 잔액을 업데이트하는 과정에서 두 스레드가 동시에 접근하면 잔액이 올바르게 업데이트되지 않을 수 있습니다. 이때 뮤텍스를 사용하면 한 번에 하나의 스레드만 잔액을 업데이트하도록 보장할 수 있습니다.

뮤텍스는 멀티스레드 환경에서의 안정성과 효율성을 크게 향상시키는 중요한 도구입니다.

캐시 시스템의 개요와 필요성


캐시 시스템은 컴퓨터 과학에서 자주 사용되는 성능 최적화 기법으로, 반복적으로 사용되는 데이터를 임시 저장하여 접근 속도를 높이는 역할을 합니다. 특히, 멀티스레드 환경에서는 적절한 캐시 설계가 시스템 성능과 안정성을 크게 좌우합니다.

캐시 시스템의 정의


캐시는 고속 접근이 가능한 메모리 공간에 자주 사용되는 데이터를 저장함으로써, 더 느린 기본 데이터 소스(예: 디스크 또는 네트워크)로의 접근을 줄이는 기술입니다.

캐시 시스템이 필요한 이유

  1. 성능 최적화: 데이터에 대한 반복적 접근 속도를 개선하여 처리 시간을 단축합니다.
  2. 리소스 효율성: 불필요한 데이터 조회를 줄여 리소스를 효율적으로 활용합니다.
  3. 시스템 부하 감소: 기본 데이터 소스에 대한 요청 빈도를 줄여 시스템 부하를 완화합니다.

캐시 시스템의 주요 구성 요소

  • 키-값 저장소: 데이터 항목을 키-값 쌍으로 저장하여 빠르게 검색할 수 있도록 설계합니다.
  • 적중률(Cache Hit Rate): 요청된 데이터가 캐시에 존재하는 비율로, 성능 지표로 사용됩니다.
  • 무효화 및 갱신 정책: 오래된 데이터를 갱신하거나 제거하는 메커니즘으로, 캐시 크기와 정확성을 유지합니다.

멀티스레드 환경에서의 캐시 시스템


멀티스레드 환경에서는 여러 스레드가 동시에 캐시에 접근할 수 있으므로, 동기화 메커니즘이 필수적입니다. 뮤텍스는 이러한 환경에서 캐시의 일관성을 유지하며, 충돌을 방지하는 데 효과적입니다.

캐시 시스템은 데이터 접근 성능을 크게 개선할 수 있는 강력한 도구로, 적절한 설계와 구현이 요구됩니다.

C 언어로 뮤텍스 초기화 및 사용 방법


C 언어에서 뮤텍스를 사용하려면 POSIX 스레드(Pthreads) 라이브러리를 활용해야 합니다. 이 섹션에서는 뮤텍스 초기화, 잠금, 해제 및 소멸 과정을 다룹니다.

뮤텍스 초기화


뮤텍스를 사용하려면 먼저 pthread_mutex_t 타입의 뮤텍스 객체를 선언하고 초기화해야 합니다. 초기화는 정적 초기화 또는 동적 초기화 중 하나를 선택할 수 있습니다.

#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 정적 초기화
// 또는 동적 초기화
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

뮤텍스 잠금


뮤텍스를 잠그면 다른 스레드가 동일한 자원에 접근하지 못하도록 차단할 수 있습니다.

pthread_mutex_lock(&mutex);  // 잠금 수행

뮤텍스 해제


작업이 끝난 후 뮤텍스를 해제하여 다른 스레드가 자원에 접근할 수 있도록 해야 합니다.

pthread_mutex_unlock(&mutex);  // 잠금 해제

뮤텍스 소멸


더 이상 뮤텍스가 필요하지 않을 경우, 자원을 반환하기 위해 뮤텍스를 소멸시켜야 합니다.

pthread_mutex_destroy(&mutex);

뮤텍스 사용 예제


아래는 간단한 멀티스레드 프로그램에서 뮤텍스를 사용하는 예제입니다:

#include <stdio.h>
#include <pthread.h>

pthread_mutex_t mutex;
int shared_resource = 0;

void* thread_function(void* arg) {
    pthread_mutex_lock(&mutex);
    shared_resource++;
    printf("Thread %ld: Resource = %d\n", (long)arg, shared_resource);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t threads[2];
    pthread_mutex_init(&mutex, NULL);

    for (long i = 0; i < 2; i++) {
        pthread_create(&threads[i], NULL, thread_function, (void*)i);
    }

    for (int i = 0; i < 2; i++) {
        pthread_join(threads[i], NULL);
    }

    pthread_mutex_destroy(&mutex);
    return 0;
}

이 코드는 두 개의 스레드가 뮤텍스를 사용하여 공유 자원 shared_resource를 안전하게 업데이트하는 방법을 보여줍니다.

유의 사항

  • 뮤텍스를 적절히 해제하지 않으면 데드락이 발생할 수 있습니다.
  • 뮤텍스 초기화와 소멸은 반드시 짝을 맞춰 사용해야 합니다.

뮤텍스는 멀티스레드 환경에서 데이터 일관성을 보장하는 강력한 도구로, 올바른 사용이 중요합니다.

캐시 시스템 설계: 데이터 구조 선택


효율적인 캐시 시스템을 구현하려면 적절한 데이터 구조를 선택하는 것이 중요합니다. 데이터 구조는 성능, 사용 사례, 메모리 효율성에 큰 영향을 미칩니다.

캐시 시스템에서 사용하는 주요 데이터 구조

1. 배열(Array)

  • 장점: 간단한 구조로 구현이 쉬움.
  • 단점: 고정된 크기를 가지며, 삽입 및 삭제가 비효율적일 수 있음.
  • 사용 예시: 작은 크기의 정적 데이터 캐시에 적합.

2. 연결 리스트(Linked List)

  • 장점: 동적 크기 조정 가능, 삽입/삭제가 빠름.
  • 단점: 검색 시간이 길어질 수 있음.
  • 사용 예시: 최근에 사용된 데이터의 순서를 유지해야 하는 LRU(Least Recently Used) 캐시.

3. 해시 테이블(Hash Table)

  • 장점: O(1)에 가까운 검색 속도 제공.
  • 단점: 메모리 사용량이 많을 수 있음, 해시 충돌 가능성.
  • 사용 예시: 키-값 쌍을 빠르게 검색해야 하는 캐시.

4. 이진 탐색 트리(Binary Search Tree)

  • 장점: 정렬된 데이터를 효율적으로 관리 가능.
  • 단점: 균형이 깨질 경우 성능 저하.
  • 사용 예시: 범위 기반 검색이 필요한 경우.

대표적인 캐시 알고리즘과 데이터 구조


캐시 시스템에서는 데이터 구조와 함께 적절한 캐시 알고리즘을 선택하는 것도 중요합니다.

  • FIFO(First In First Out): 가장 먼저 삽입된 데이터를 가장 먼저 제거.
  • 구현: 큐(Queue) 사용.
  • LRU(Least Recently Used): 가장 오래 사용되지 않은 데이터를 제거.
  • 구현: 해시 테이블과 이중 연결 리스트의 조합.
  • LFU(Least Frequently Used): 가장 사용 빈도가 낮은 데이터를 제거.
  • 구현: 해시 테이블과 최소 힙(Min-Heap)의 조합.

예제: LRU 캐시 데이터 구조


LRU 캐시는 해시 테이블과 이중 연결 리스트를 조합하여 다음을 구현합니다:

  1. 해시 테이블: 데이터의 빠른 검색.
  2. 이중 연결 리스트: 최근 사용 순서를 유지하며 삽입/삭제 효율성 제공.
typedef struct Node {
    int key;
    int value;
    struct Node* prev;
    struct Node* next;
} Node;

typedef struct {
    Node* head;
    Node* tail;
    int capacity;
    int size;
} LRUCache;

// 해시 테이블과 연결 리스트를 활용한 구조체 정의 및 함수 구현.

캐시 시스템의 데이터 구조는 사용 사례와 성능 요구 사항에 따라 신중히 선택해야 하며, 복잡한 캐시 로직은 데이터 구조의 조합을 통해 구현될 수 있습니다.

뮤텍스를 활용한 캐시 읽기/쓰기 동작 구현


뮤텍스를 사용하면 멀티스레드 환경에서 캐시 시스템의 데이터 일관성과 안정성을 유지할 수 있습니다. 이 섹션에서는 뮤텍스를 활용해 캐시의 읽기 및 쓰기 동작을 구현하는 방법을 살펴봅니다.

캐시 읽기 동작


캐시 읽기 작업은 읽기 전용이므로, 멀티스레드 환경에서 동시 접근이 가능하도록 설계할 수 있습니다. 그러나 데이터 갱신이나 삭제 작업이 동시에 발생할 가능성이 있다면, 읽기 작업에도 동기화가 필요합니다.

예제 코드:

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

typedef struct {
    int key;
    int value;
} CacheItem;

CacheItem cache[100];  // 간단한 캐시 배열
pthread_mutex_t cache_mutex;  // 뮤텍스 선언

int read_from_cache(int key) {
    pthread_mutex_lock(&cache_mutex);  // 뮤텍스 잠금
    for (int i = 0; i < 100; i++) {
        if (cache[i].key == key) {
            int value = cache[i].value;
            pthread_mutex_unlock(&cache_mutex);  // 뮤텍스 해제
            return value;  // 캐시 값 반환
        }
    }
    pthread_mutex_unlock(&cache_mutex);  // 뮤텍스 해제
    return -1;  // 키가 없는 경우
}

캐시 쓰기 동작


쓰기 작업은 캐시 데이터를 변경하므로, 반드시 동기화가 필요합니다. 멀티스레드 환경에서는 뮤텍스를 사용하여 동시에 여러 스레드가 쓰기 작업을 수행하지 못하도록 제한합니다.

예제 코드:

void write_to_cache(int key, int value) {
    pthread_mutex_lock(&cache_mutex);  // 뮤텍스 잠금
    for (int i = 0; i < 100; i++) {
        if (cache[i].key == key || cache[i].key == 0) {  // 빈 슬롯 찾기
            cache[i].key = key;
            cache[i].value = value;
            break;
        }
    }
    pthread_mutex_unlock(&cache_mutex);  // 뮤텍스 해제
}

캐시 읽기와 쓰기의 동시 처리


캐시 읽기와 쓰기 작업이 동시에 발생할 수 있으므로, 읽기-쓰기 잠금을 통해 성능을 개선할 수 있습니다. 예를 들어, POSIX에서 제공하는 pthread_rwlock을 사용하면 읽기 작업은 동시에 처리하고 쓰기 작업은 배타적으로 처리할 수 있습니다.

뮤텍스 기반 캐시 시스템에서 주의 사항

  1. 데드락 방지: 항상 동일한 순서로 뮤텍스를 잠그고 해제합니다.
  2. 성능 최적화: 과도한 잠금은 성능 병목을 초래할 수 있으므로 필요 최소한의 영역에서만 잠금을 사용합니다.
  3. 에러 핸들링: pthread_mutex_lockpthread_mutex_unlock 함수의 반환값을 확인하여 잠금 상태를 항상 확인합니다.

뮤텍스 구현의 요약


뮤텍스를 활용한 읽기 및 쓰기 동작 구현은 다음과 같은 과정을 따릅니다:

  1. 뮤텍스를 초기화합니다.
  2. 공유 자원 접근 시 뮤텍스를 잠급니다.
  3. 작업이 끝나면 뮤텍스를 해제합니다.
  4. 프로그램 종료 시 뮤텍스를 소멸합니다.

뮤텍스는 데이터 충돌을 방지하고 멀티스레드 환경에서 캐시 시스템의 안정성과 효율성을 보장하는 핵심 도구입니다.

캐시 무효화 및 갱신 전략


효율적인 캐시 시스템 운영을 위해 오래된 데이터의 무효화와 최신 데이터를 유지하는 갱신 전략은 필수적입니다. 이 섹션에서는 다양한 무효화 및 갱신 전략을 소개하고 구현 방법을 다룹니다.

캐시 무효화 전략


캐시 무효화는 오래되거나 사용되지 않는 데이터를 제거하여 캐시의 공간과 정확성을 유지하는 과정입니다.

1. 시간 기반(Time-Based Invalidation)

  • 설명: 데이터가 일정 시간이 지나면 무효화됩니다.
  • 구현: 각 캐시 항목에 타임스탬프를 추가하고, 현재 시간과 비교하여 만료 여부를 확인합니다.
  • 장점: 단순하고 예측 가능.
  • 단점: 모든 데이터가 동일한 주기로 무효화될 수 있음.
if (current_time - cache_item.timestamp > EXPIRATION_TIME) {
    invalidate_cache_item(cache_item);
}

2. 사용 기반(Usage-Based Invalidation)

  • 설명: 사용 빈도가 낮은 데이터를 제거합니다.
  • 구현: 사용 횟수를 추적하거나 LRU(Least Recently Used) 알고리즘을 활용합니다.
  • 장점: 자주 사용하는 데이터를 유지.
  • 단점: 구현이 복잡할 수 있음.
remove_least_recently_used_item();

3. 명시적 무효화(Explicit Invalidation)

  • 설명: 애플리케이션 또는 관리자가 특정 조건에서 명시적으로 데이터를 무효화합니다.
  • 장점: 정확성과 제어성 보장.
  • 단점: 자동화되지 않음.
invalidate_cache_item_by_key(key);

캐시 갱신 전략


캐시 갱신은 오래된 데이터를 최신 상태로 업데이트하여 정확성을 보장하는 과정입니다.

1. 쓰기 스루(Write-Through)

  • 설명: 데이터를 캐시에 기록하는 동시에 기본 데이터 소스에도 기록합니다.
  • 장점: 데이터 일관성 보장.
  • 단점: 쓰기 속도가 느릴 수 있음.
write_to_cache_and_source(key, value);

2. 쓰기 백(Write-Back)

  • 설명: 데이터를 캐시에만 기록하고, 일정 주기 또는 조건에 따라 기본 소스에 기록합니다.
  • 장점: 쓰기 성능 향상.
  • 단점: 데이터 손실 가능성.
cache_item.dirty = true;  // 캐시 데이터를 나중에 동기화

3. 온디맨드 갱신(On-Demand Refresh)

  • 설명: 데이터가 요청될 때 기본 데이터 소스에서 최신 데이터를 가져옵니다.
  • 장점: 정확성 보장.
  • 단점: 요청 지연 가능.
if (is_data_stale(key)) {
    refresh_cache_item(key);
}

무효화 및 갱신 전략의 조합


캐시 시스템은 위 전략 중 하나를 단독으로 사용하거나, 시스템 요구사항에 따라 여러 전략을 조합하여 설계할 수 있습니다. 예를 들어, LRU 기반 무효화와 쓰기 스루를 결합하면 자주 사용하는 데이터를 유지하며 데이터 일관성을 확보할 수 있습니다.

유의 사항

  1. 성능과 정확성의 균형: 갱신 주기와 무효화 정책은 시스템의 성능 요구사항과 데이터 정확성 사이에서 균형을 맞춰야 합니다.
  2. 캐시 크기 관리: 캐시 크기를 적절히 설정하여 메모리 부족을 방지합니다.
  3. 동기화 문제 방지: 멀티스레드 환경에서 갱신 작업에 뮤텍스를 활용해 동기화 문제를 해결합니다.

효율적인 무효화와 갱신 전략은 캐시 시스템의 성능과 데이터 정확성을 유지하는 데 필수적입니다.

성능 테스트와 디버깅 방법


뮤텍스 기반 캐시 시스템의 효율성과 안정성을 보장하려면 성능 테스트와 디버깅이 필수적입니다. 이 섹션에서는 성능을 평가하는 주요 방법과 디버깅 절차를 다룹니다.

성능 테스트

1. 테스트 메트릭


캐시 시스템의 성능을 평가하기 위해 다음 메트릭을 사용합니다:

  • 캐시 적중률(Cache Hit Rate): 요청된 데이터가 캐시에 존재하는 비율.
  • 평균 응답 시간(Average Response Time): 캐시가 데이터 요청에 응답하는 데 걸리는 평균 시간.
  • 처리량(Throughput): 단위 시간당 처리된 요청의 수.
  • 자원 사용률(Resource Utilization): CPU, 메모리, 네트워크 사용량.

2. 성능 테스트 도구 및 기법

  • 로드 테스트(Load Testing): 다양한 부하 상황에서 시스템의 응답성을 평가합니다.
  • 예: 수백 개의 동시 요청을 생성하여 시스템이 처리할 수 있는 최대 부하를 테스트.
  • 스트레스 테스트(Stress Testing): 시스템의 한계를 넘어서는 부하를 가하여 안정성을 평가합니다.
  • 프로파일링(Profile Analysis): 코드의 병목 지점을 찾아 최적화합니다.
  • 도구: gprof, valgrindcallgrind 모드.

3. 샘플 테스트 코드


캐시의 적중률을 측정하는 코드 예제:

#include <stdio.h>

int cache_hits = 0;
int cache_misses = 0;

void test_cache_performance() {
    // 가상 데이터 요청
    for (int i = 0; i < 1000; i++) {
        if (read_from_cache(i) != -1) {
            cache_hits++;
        } else {
            cache_misses++;
        }
    }

    double hit_rate = (double)cache_hits / (cache_hits + cache_misses);
    printf("Cache Hit Rate: %.2f%%\n", hit_rate * 100);
}

디버깅 방법

1. 일반적인 문제와 해결

  • 데드락: 뮤텍스가 해제되지 않아 스레드가 영구적으로 대기 상태에 빠짐.
  • 해결: 잠금-해제 순서를 검토하고 데드락 예방 전략 사용.
  • 경쟁 조건: 두 개 이상의 스레드가 동시에 자원을 변경하여 예기치 않은 결과가 발생.
  • 해결: 모든 공유 자원 접근에 뮤텍스를 적용.
  • 메모리 누수: 뮤텍스나 캐시 항목이 적절히 해제되지 않음.
  • 해결: valgrind와 같은 도구를 사용해 메모리 누수 탐지.

2. 디버깅 도구

  • GDB(Debugger): 런타임 오류를 추적하고 문제 지점을 식별.
  • Valgrind: 메모리 누수 및 경쟁 조건을 감지.
  • ThreadSanitizer: 스레드 관련 문제를 효과적으로 탐지.

3. 로깅 및 트레이싱


문제 해결을 위해 로그를 기록하여 시스템 동작을 추적합니다.

#include <stdio.h>

void log_message(const char* message) {
    FILE* log_file = fopen("cache_log.txt", "a");
    fprintf(log_file, "%s\n", message);
    fclose(log_file);
}

성능 최적화 팁

  1. 뮤텍스 사용 최소화: 잠금 범위를 최소화하여 병렬성을 최대화합니다.
  2. 데이터 구조 최적화: 적절한 데이터 구조를 선택하여 검색 및 삽입 시간을 줄입니다.
  3. 멀티스레드 분산: 요청 부하를 여러 스레드에 고르게 분산합니다.

성능 테스트와 디버깅은 뮤텍스 기반 캐시 시스템의 안정성과 효율성을 보장하기 위한 중요한 과정입니다. 정기적인 테스트와 철저한 디버깅을 통해 시스템을 최적화할 수 있습니다.

실전 예제: 간단한 캐시 시스템 구현


이 섹션에서는 뮤텍스를 활용해 간단한 캐시 시스템을 C 언어로 구현하는 예제를 소개합니다. 이 캐시는 키-값 쌍을 저장하며, LRU(Least Recently Used) 알고리즘을 기반으로 작동합니다.

캐시 시스템 설계

  • 데이터 구조:
  • HashMap으로 키를 빠르게 검색.
  • 이중 연결 리스트를 사용해 데이터의 사용 순서를 추적(LRU 구현).
  • 뮤텍스 사용:
  • 읽기와 쓰기 작업의 동기화를 위해 뮤텍스를 사용.

코드 구현

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

typedef struct Node {
    int key;
    int value;
    struct Node* prev;
    struct Node* next;
} Node;

typedef struct {
    int capacity;
    int size;
    Node* head;
    Node* tail;
    Node** hash_map;  // 키 검색을 위한 해시 맵
    pthread_mutex_t mutex;
} LRUCache;

// 노드 생성
Node* create_node(int key, int value) {
    Node* node = (Node*)malloc(sizeof(Node));
    node->key = key;
    node->value = value;
    node->prev = node->next = NULL;
    return node;
}

// 캐시 초기화
LRUCache* create_cache(int capacity) {
    LRUCache* cache = (LRUCache*)malloc(sizeof(LRUCache));
    cache->capacity = capacity;
    cache->size = 0;
    cache->head = cache->tail = NULL;
    cache->hash_map = (Node**)calloc(100, sizeof(Node*)); // 간단한 해시 맵
    pthread_mutex_init(&cache->mutex, NULL);
    return cache;
}

// 노드를 가장 최근으로 이동
void move_to_head(LRUCache* cache, Node* node) {
    if (cache->head == node) return;

    if (node->prev) node->prev->next = node->next;
    if (node->next) node->next->prev = node->prev;

    if (cache->tail == node) cache->tail = node->prev;

    node->next = cache->head;
    if (cache->head) cache->head->prev = node;

    cache->head = node;
    node->prev = NULL;
}

// 캐시에 값 읽기
int get(LRUCache* cache, int key) {
    pthread_mutex_lock(&cache->mutex);

    Node* node = cache->hash_map[key % 100];
    while (node && node->key != key) {
        node = node->next;
    }

    if (!node) {
        pthread_mutex_unlock(&cache->mutex);
        return -1; // 캐시 미스
    }

    move_to_head(cache, node); // LRU 갱신
    pthread_mutex_unlock(&cache->mutex);
    return node->value;
}

// 캐시에 값 쓰기
void put(LRUCache* cache, int key, int value) {
    pthread_mutex_lock(&cache->mutex);

    Node* node = cache->hash_map[key % 100];
    while (node && node->key != key) {
        node = node->next;
    }

    if (node) {
        node->value = value;
        move_to_head(cache, node);
    } else {
        Node* new_node = create_node(key, value);
        if (cache->size == cache->capacity) {
            cache->hash_map[cache->tail->key % 100] = NULL;
            Node* old_tail = cache->tail;
            cache->tail = old_tail->prev;
            if (cache->tail) cache->tail->next = NULL;
            free(old_tail);
            cache->size--;
        }

        new_node->next = cache->head;
        if (cache->head) cache->head->prev = new_node;
        cache->head = new_node;

        if (!cache->tail) cache->tail = new_node;

        cache->hash_map[key % 100] = new_node;
        cache->size++;
    }

    pthread_mutex_unlock(&cache->mutex);
}

// 캐시 삭제
void free_cache(LRUCache* cache) {
    Node* current = cache->head;
    while (current) {
        Node* temp = current;
        current = current->next;
        free(temp);
    }
    free(cache->hash_map);
    pthread_mutex_destroy(&cache->mutex);
    free(cache);
}

테스트 코드

int main() {
    LRUCache* cache = create_cache(2);

    put(cache, 1, 10);
    put(cache, 2, 20);
    printf("Get 1: %d\n", get(cache, 1));  // 10
    put(cache, 3, 30);  // 2 제거
    printf("Get 2: %d\n", get(cache, 2));  // -1
    printf("Get 3: %d\n", get(cache, 3));  // 30

    free_cache(cache);
    return 0;
}

설명

  • LRU 동작: 가장 오래 사용되지 않은 항목이 제거됩니다.
  • 동기화: pthread_mutex_t를 사용해 읽기 및 쓰기 작업 간 충돌을 방지합니다.

이 예제는 멀티스레드 환경에서 안정적이고 효율적인 캐시 시스템 구현의 기본을 보여줍니다.

요약


본 기사에서는 C 언어를 활용한 뮤텍스 기반 캐시 시스템 구현에 대해 다뤘습니다. 멀티스레드 환경에서 뮤텍스를 통해 데이터 일관성을 유지하며, 캐시 시스템의 설계, 구현, 성능 테스트, 그리고 디버깅 방법까지 자세히 설명했습니다. 또한, 실전 예제를 통해 LRU 알고리즘을 사용한 캐시 시스템을 구현하며, 효율적인 데이터 접근과 동기화의 중요성을 확인했습니다. 이를 통해 안정적이고 최적화된 캐시 시스템 설계와 구현을 위한 기본적인 지식을 습득할 수 있습니다.

목차