시프 23-Pthreads

강준호·2024년 6월 2일

시스템프로그래밍

목록 보기
15/18

POSIX 스레드와 동기화

POSIX 스레드 (Pthreads)

POSIX 스레드 개요

  • POSIX 스레드 API는 UNIX에 스레딩 기능을 추가합니다.
  • Pthreads 또는 pthreads라고도 불립니다.
  • 초기 UNIX는 동시성을 위해 프로세스 모델만 제공했습니다.
  • POSIX 스레드는 프로세스처럼 보이지만 더 많은 리소스를 공유합니다.
    • 함수로 시작합니다.
    • 프로세스처럼 회수(join)되어야 합니다.
    • 종료 상태를 제공합니다.

POSIX 동기화

  • Pthreads는 동기화 메커니즘도 제공합니다.
  • 다양한 옵션을 포함합니다:
    • 뮤텍스
    • 세마포어
    • 조건 변수
    • 스레드 조인
    • 메모리 장벽

컴파일 옵션

Pthreads 컴파일

  • Pthreads는 추가 컴파일 옵션이 필요할 수 있습니다.
  • 현대적인 Linux에서는 컴파일 및 링크 시 -pthread 옵션을 사용합니다.
  • 다른 시스템에서는 다른 옵션이 필요할 수 있습니다:
    • 다른 컴파일러 또는 링커 옵션 제공 (예: -pthreads)
    • 전처리기 정의로 컴파일 (예: -DPTHREAD, -D_REENTRANT)
    • 라이브러리로 링크 (예: -lpthread)

스레드 생성

스레드 생성 함수

#include <pthread.h> 

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_function)(void *), void *arg);
  • 생성된 스레드는 start_function 함수에서 시작합니다.
  • arg로 전달된 데이터를 인수로 받습니다.

Pthread 객체 선언

  • 스레드 및 기타 Pthread 객체는 값으로 선언됩니다.
  • 예:
pthread_t thread;
pthread_create(&thread, NULL, thread_function, NULL);
  • 동적 할당 없이 생성할 수 있습니다.

스레드 함수

스레드 시작 함수

  • 스레드 시작 함수는 다음 서명을 가집니다:
void *(*start_function)(void *);
  • 이 함수는 단일 void * 인수를 받아 void *를 반환합니다.
  • 예:
void *thread_main(void *arg) { return NULL; }

스레드 동작

  • pthread_create()가 호출되면:
    • 새로운 실행 컨텍스트가 생성됩니다 (스택 포함).
    • 커널로부터 사용자 공간이나 요청하여 새로운 흐름이 생성됩니다 (Linux는 후자를 수행).
    • 새로운 흐름이 제공된 함수와 인수를 호출하게 됩니다.

스레드 속성

스레드 속성 개요

  • pthread_create() 함수는 스레드 속성 객체를 수락합니다.
  • 이 객체는 pthread_attr_t 타입입니다.
  • 이 인수에 NULL을 전달하면 기본 속성이 사용됩니다.
  • 스레드 속성에는 다음이 포함됩니다:
    • 프로세서 선호도
    • 스레드의 원하는 스케줄러 및 구성
    • 새 스레드의 분리 상태

분리 상태

  • 여기서는 분리 상태만 다룹니다.

스레드 종료

스레드 종료 방법

  • POSIX 스레드는 여러 가지 방법으로 종료할 수 있습니다:

    • 애플리케이션이 종료될 때
    • pthread_exit() 호출
    • 스레드 시작 함수에서 반환
    • 다른 스레드가 pthread_cancel()을 호출하여 취소할 때
  • 분리되지 않은 스레드는 조인될 때까지 좀비 상태가 됩니다.

스레드 조인 및 분리

스레드 조인

  • 스레드는 동기식 연산으로 조인될 수 있습니다.
#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
  • 스레드 조인:
    • 호출자를 블록하여 스레드가 종료될 때까지 기다립니다.
    • 스레드의 종료 상태를 가져옵니다.
    • 프로세스의 wait()와 유사합니다.

스레드 분리

  • 스레드는 생성 시 속성으로 또는 pthread_detach()를 호출하여 분리될 수 있습니다.
  • 분리된 스레드는 조인될 수 없으며 좀비 상태가 되지 않습니다.

POSIX 뮤텍스

뮤텍스 개요

  • POSIX 뮤텍스는 pthread_mutex_t 타입입니다.
  • 기본 뮤텍스 기능을 제공하며 다음과 같은 특징이 있습니다:
    • 선택적 재귀 잠금 감지
    • 뮤텍스가 잠금될 수 있는지 여부와 상관없이 즉시 반환하는 시도 잠금 연산

뮤텍스 초기화

  • POSIX 뮤텍스는 정적 및 동적 초기화자가 있습니다:
#include <pthread.h>

pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
  • 정적 초기화자는 컴파일 시간 초기화자에만 사용될 수 있습니다.
  • 동적 초기화자는 뮤텍스를 구성할 속성을 수락합니다 (기본 동작을 원할 경우 NULL 전달).

뮤텍스 연산

  • 뮤텍스는 잠금 또는 잠금 해제할 수 있습니다:
#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_trylock()은 항상 즉시 반환합니다:
    • 뮤텍스가 이미 잠겨 있으면 EBUSY를 반환합니다.
    • 뮤텍스가 잠겨 있지 않으면 잠그고 0을 반환합니다.

뮤텍스 소멸

  • 뮤텍스를 사용한 후에는 소멸해야 합니다.
  • Linux에서 뮤텍스 소멸은 사실상 무연산입니다.
  • 다른 플랫폼에서는 뮤텍스에 리소스를 연관시킬 수 있습니다.
  • 잠긴 뮤텍스를 소멸하는 것은 오류입니다.

기본 뮤텍스 동작

  • 기본 뮤텍스는 재귀 잠금을 허용하지 않을 수 있습니다.
  • 다음 코드는 교착 상태를 일으킬 수 있습니다 (Linux에서는 교착 상태 발생):
void deadlock() {
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    pthread_mutex_lock(&mutex);
}
  • 뮤텍스를 재귀 속성으로 초기화할 수 있습니다.
  • 재귀 뮤텍스는 잠금 횟수를 유지하며 위의 코드는 단순히 두 번 잠금 해제를 요구합니다.

조건 변수 (Condition Variables)

조건 변수 개요

  • POSIX 조건 변수는 뮤텍스와 함께 작동합니다.
  • 스레드는 조건 변수를 기다리기 위해 뮤텍스를 소유해야 합니다.

조건 변수 대기

  • 조건 변수를 기다리는 동안 원자적으로 다음을 수행합니다:
    • 뮤텍스를 잠금 해제
    • 조건이 신호될 때까지 스레드를 잠재웁니다
  • 스레드는 조건 변수에서 대기 중인 모든 스레드를 신호할 수 있습니다.

조건 변수 생성

#include <pthread.h>

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);

조건 변수 대기 예시

extern pthread_mutex_t lock;
extern pthread_cond_t cond;
extern bool done;

void *block_until_done(void *ignored) {
    pthread_mutex_lock(&lock);
    while (!done) {
        pthread_cond_wait(&cond, &lock);
    }
    pthread_mutex_unlock(&lock);
}

조건 변수 신호

  • 조건 변수는 다음을 신호할 수 있습니다:
    • 하나의 대기 스레드
    • 모든 대기 스레드
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
  • 대기 중인 스레드가 없으면 아무 작업도 하지 않습니다.
  • 공유 상태를 보호하는 뮤텍스를 적절히 사용해야 합니다.

조건 변수 신호 예시

extern pthread_mutex_t lock;
extern pthread_cond_t cond;
extern bool done;

void signal_done() {
    pthread_mutex_lock(&lock);
    done = true;
    pthread_mutex_unlock(&lock);
    pthread_cond_signal(&cond);
}

POSIX 세마포어

세마포어 개요

  • POSIX 세마포어는 스레드 또는 프로세스 간에 작동할 수 있습니다.
  • 세마포어는 카운팅 세마포어 의미론을 제공합니다.
  • POSIX 세마포어는 pthread.h에 포함되지 않으며, pthread_로 시작하지 않습니다.

세마포어 생성

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
  • pshared가 true이면 세마포어는 프로세스 간에 사용할 수 있습니다.
  • 이를 위해 세마포어는 공유 메모리에 위치해야 합니다.
  • 주어진 값은 세마포어의 초기 카운트입니다.

세마포어 조작

#include <semaphore.h>

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t *sem);
  • sem_wait()는 Dijkstra의 P() 연산에 해당하며, sem_post()는 V() 연산에 해당합니다.
  • sem_trywait()pthread_mutex_trylock()과 유사합니다:
    • 세마포어를 감소시킬 수 없는 경우 즉시 반환합니다.
    • 성공하면 0을 반환합니다.
    • 실패하면 EAGAIN을 반환합니다.

종합 예시: 조건 변수와 뮤텍스를 사용한 간단한 프로그램

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

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool done = false;

void *block_until_done(void *ignored) {
    pthread_mutex_lock(&lock);
    while (!done) {
        pthread_cond_wait(&cond, &lock);
    }
    pthread_mutex_unlock(&lock);
    return NULL;
}

void signal_done() {
    pthread_mutex_lock(&lock);
    done = true;
    pthread_mutex_unlock(&lock);
    pthread_cond_signal(&cond);
}

int main(int argc, char *argv[]) {
    pthread_t t;
    pthread_create(&t, NULL, block_until_done, NULL);
    usleep(100000);
    signal_done();
    pthread_join(t, NULL);
    return 0;
}
  • 이 프로그램은 조건 변수와 뮤텍스를 사용하여 done 플래그가 설정될 때까지 스레드를 블록합니다.

세마포어를 사용한 예제

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

sem_t sem;

void* worker(void* arg) {
    sem_wait(&sem);
    printf("Entered critical section\n");
    // Critical section
    sleep(2); 
    printf("Leaving critical section\n");
    sem_post(&sem);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    sem_init(&sem, 0, 1);

    pthread_create(&t1, NULL, worker, NULL);
    sleep(1);  // Ensure t1 starts first
    pthread_create(&t2, NULL, worker, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    sem_destroy(&sem);
    return 0;
}
  • 이 프로그램은 세마포어를 사용하여 한 번에 하나의 스레드만 임계 구역에 들어가도록 합니다.

요약

  • POSIX 스레드(pthreads) API는 스레드 추상화를 제공합니다.
  • POSIX는 여러 동기화 원시 기능을 제공합니다:
    • 뮤텍스
    • 세마포어
    • 조건 변수
    • 스레드 조인
  • 뮤텍스는 임계 구역을 보호하며 상호 배제를 보장합니다.
  • 조건 변수는 특정 조건이 충족될 때까지 스레드를 블록합니다.
  • 세마포어는 카운팅 메커니즘을 통해 리소스 접근을 제어합니다.
  • 스레드 조인은 스레드가 종료될 때까지 대기하는 동기화 메커니즘입니다.

0개의 댓글