스레드 동기화는 실시간 시스템에서 매우 중요한 역할을 한다. Preempt RT에서 실시간 스레드 프로그래밍을 할 때, 여러 스레드가 공유 자원에 접근하거나, 특정 순서대로 작업을 처리해야 할 때 동기화가 필수적이다. 적절한 동기화 기법을 사용하지 않으면 우선순위 역전, 데드락, 경쟁 상태 등의 문제가 발생할 수 있다.

뮤텍스(Mutex)

뮤텍스는 Mutual Exclusion의 약자로, 여러 스레드가 공유 자원에 동시 접근하지 못하도록 하는 동기화 객체이다. 뮤텍스는 다음과 같은 특징을 갖는다:

뮤텍스 사용 예제

다음은 Preempt RT에서 뮤텍스를 사용한 간단한 C 코드 예제이다:

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

pthread_mutex_t mutex;

void* thread_function(void* arg) {
    pthread_mutex_lock(&mutex);
    // 공유 자원 접근
    printf("Thread %ld: critical section\n", (long)arg);
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&thread1, NULL, thread_function, (void*)1);
    pthread_create(&thread2, NULL, thread_function, (void*)2);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    pthread_mutex_destroy(&mutex);
    return 0;
}

위 예제에서 pthread_mutex_lock 함수를 통해 뮤텍스를 잠그고, 임계 영역을 보호한다. pthread_mutex_unlock 함수는 뮤텍스를 해제하여 다른 스레드가 자원에 접근할 수 있도록 한다.

세마포어(Semaphore)

세마포어는 카운터 개념을 기반으로 한 동기화 메커니즘으로, 특정 자원의 접근을 제한하는 데 사용된다. 세마포어는 다음과 같은 특징을 갖는다:

이진 세마포어(Binary Semaphore)

이진 세마포어는 카운터 값이 0 또는 1만 가질 수 있는 세마포어이다. 사실상 뮤텍스와 유사한 역할을 하지만, 소유권 개념이 없고, 같은 세마포어를 여러 스레드가 해제할 수 있다.

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

sem_t semaphore;

void* thread_function(void* arg) {
    sem_wait(&semaphore);
    // 공유 자원 접근
    printf("Thread %ld: critical section\n", (long)arg);
    sem_post(&semaphore);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    sem_init(&semaphore, 0, 1);

    pthread_create(&thread1, NULL, thread_function, (void*)1);
    pthread_create(&thread2, NULL, thread_function, (void*)2);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    sem_destroy(&semaphore);
    return 0;
}

위 코드에서 sem_wait은 세마포어를 잠그는 함수이며, 세마포어의 카운터 값을 감소시킨다. sem_post는 세마포어를 해제하여 카운터 값을 증가시킨다.

세마포어의 유형

세마포어는 주로 두 가지 유형으로 나뉜다: 이진 세마포어카운팅 세마포어. 이미 이진 세마포어에 대해 논의했으므로, 이제 카운팅 세마포어에 대해 설명하겠다.

카운팅 세마포어(Counting Semaphore)

카운팅 세마포어는 카운터의 값이 0 이상의 정수 범위를 가지며, 동시에 여러 스레드가 자원에 접근할 수 있는 개수를 제어할 수 있다. 예를 들어, 특정 자원에 동시에 5개의 스레드만 접근하도록 허용하려면 세마포어를 5로 초기화한다.

카운팅 세마포어는 다음과 같이 동작한다:

카운팅 세마포어 예제

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

#define NUM_RESOURCES 3

sem_t semaphore;

void* thread_function(void* arg) {
    sem_wait(&semaphore);
    // 공유 자원 접근
    printf("Thread %ld: using resource\n", (long)arg);
    sleep(1); // 자원 사용 시뮬레이션
    printf("Thread %ld: releasing resource\n", (long)arg);
    sem_post(&semaphore);
    return NULL;
}

int main() {
    pthread_t threads[5];

    sem_init(&semaphore, 0, NUM_RESOURCES);

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

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

    sem_destroy(&semaphore);
    return 0;
}

이 예제에서 NUM_RESOURCES는 자원의 개수를 나타내며, 최대 3개의 스레드가 동시에 자원을 사용할 수 있다. 각 스레드는 세마포어의 sem_wait을 호출하여 자원에 접근하고, 사용 후 sem_post를 호출하여 자원을 해제한다. 세마포어 카운터는 이 과정을 통해 동적으로 변화하며, 자원의 동시 접근을 제어한다.

우선순위 역전 문제와 해결 방법

스레드 동기화와 관련된 중요한 문제 중 하나는 우선순위 역전(Priority Inversion)이다. 우선순위 역전은 낮은 우선순위를 가진 스레드가 높은 우선순위의 스레드보다 먼저 실행되어야 하는 상황에서 발생한다. 이는 실시간 시스템에서 치명적인 문제로 이어질 수 있다.

우선순위 역전의 발생

우선순위 역전은 다음과 같은 시나리오에서 발생할 수 있다:

  1. 높은 우선순위 스레드 \mathbf{T_H}는 공유 자원에 접근하기 위해 뮤텍스 잠금을 요청한다.
  2. 낮은 우선순위 스레드 \mathbf{T_L}가 이미 그 자원에 대한 뮤텍스를 소유하고 있다.
  3. 중간 우선순위를 가진 스레드 \mathbf{T_M}가 실행되며, 높은 우선순위 스레드 \mathbf{T_H}는 자원을 기다리며 블로킹 상태에 있다.
  4. 이 경우, \mathbf{T_H}\mathbf{T_M}에 의해 역전되고, \mathbf{T_L}이 자원을 해제하기 전까지 실행되지 않는다.

우선순위 상속 기법

우선순위 역전을 해결하기 위해 Preempt RT에서는 우선순위 상속(Priority Inheritance) 기법을 사용한다. 이 기법의 주요 아이디어는 다음과 같다:

이 방식은 우선순위 역전의 시간을 최소화하며, 실시간 시스템의 신뢰성을 보장하는 데 필수적이다.

우선순위 상속 기법의 구현 예제

다음은 우선순위 상속이 적용된 뮤텍스의 사용을 시뮬레이션한 C 코드 예제이다. 이 코드는 일반적으로 우선순위 역전이 발생할 수 있는 상황을 설명한다.

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

pthread_mutex_t mutex;

void* low_priority_thread(void* arg) {
    struct sched_param param;
    param.sched_priority = 10;
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

    pthread_mutex_lock(&mutex);
    printf("Low priority thread: locked mutex\n");

    // 긴 작업 시뮬레이션
    sleep(2);

    printf("Low priority thread: releasing mutex\n");
    pthread_mutex_unlock(&mutex);

    return NULL;
}

void* high_priority_thread(void* arg) {
    struct sched_param param;
    param.sched_priority = 90;
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

    // 짧은 지연 후 뮤텍스 요청
    sleep(1);
    printf("High priority thread: trying to lock mutex\n");

    pthread_mutex_lock(&mutex);
    printf("High priority thread: locked mutex\n");
    pthread_mutex_unlock(&mutex);

    return NULL;
}

void* medium_priority_thread(void* arg) {
    struct sched_param param;
    param.sched_priority = 50;
    pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

    // 중간 작업 시뮬레이션
    printf("Medium priority thread: doing work\n");
    sleep(2);
    printf("Medium priority thread: done with work\n");

    return NULL;
}

int main() {
    pthread_t low, high, medium;

    pthread_mutexattr_t mutex_attr;
    pthread_mutexattr_init(&mutex_attr);
    pthread_mutexattr_setprotocol(&mutex_attr, PTHREAD_PRIO_INHERIT);
    pthread_mutex_init(&mutex, &mutex_attr);

    pthread_create(&low, NULL, low_priority_thread, NULL);
    pthread_create(&medium, NULL, medium_priority_thread, NULL);
    pthread_create(&high, NULL, high_priority_thread, NULL);

    pthread_join(low, NULL);
    pthread_join(medium, NULL);
    pthread_join(high, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_mutexattr_destroy(&mutex_attr);

    return 0;
}

이 코드는 Preempt RT에서 제공하는 우선순위 상속 기능을 시뮬레이션한다.

우선순위 상속이 적용된 경우, Low priority threadHigh priority thread의 우선순위를 상속받아 더 빨리 실행되며, Medium priority thread가 블로킹되지 않도록 한다.

스핀락(Spinlock)

스핀락은 뮤텍스와 유사한 동기화 메커니즘이지만, 블로킹 없이 잠금이 해제될 때까지 루프를 돌며 기다리는 방식으로 동작한다. 스핀락은 다음과 같은 상황에서 유용할 수 있다:

하지만, 스핀락은 단일 코어 시스템에서는 비효율적일 수 있으며, 장시간의 잠금에서는 CPU 자원을 낭비할 수 있다.

스핀락 사용 예제

다음은 간단한 스핀락 사용 예제이다.

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

pthread_spinlock_t spinlock;

void* thread_function(void* arg) {
    pthread_spin_lock(&spinlock);
    printf("Thread %ld: acquired spinlock\n", (long)arg);

    // 작업 시뮬레이션
    sleep(1);

    printf("Thread %ld: releasing spinlock\n", (long)arg);
    pthread_spin_unlock(&spinlock);

    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    pthread_spin_init(&spinlock, PTHREAD_PROCESS_PRIVATE);

    pthread_create(&thread1, NULL, thread_function, (void*)1);
    pthread_create(&thread2, NULL, thread_function, (void*)2);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    pthread_spin_destroy(&spinlock);

    return 0;
}

이 예제에서 pthread_spin_lockpthread_spin_unlock을 사용하여 스핀락을 잠그고 해제한다. 스핀락을 사용하는 동안, 다른 스레드는 루프를 돌며 잠금이 해제되기를 기다린다.

스핀락은 짧은 시간 동안의 잠금을 효율적으로 처리할 수 있지만, 장시간의 잠금이 예상될 경우에는 뮤텍스와 같은 다른 동기화 기법이 더 적절할 수 있다.