Skip to content

Latest commit

 

History

History
158 lines (130 loc) · 9.3 KB

File metadata and controls

158 lines (130 loc) · 9.3 KB

1. 스레드(Thread) 개요 및 POSIX 표준

  • POSIX (Pthreads) 정의: Portable Operating System Interface의 약어로, 유닉스 계열 운영체제 간의 호환성을 보장하기 위해 IEEE에서 책정한 표준 인터페이스 규격임.
  • 도입 목적:
    • 이식성(Portability): 다양한 OS 환경에서 동일한 스레드 코드를 수정 없이 컴파일 및 실행할 수 있도록 보장함.
    • 표준화: 스레드 생성 및 관리에 대한 일관된 함수 인터페이스를 제공하여 개발 안정성을 확보함.
  • 계층 구조: 운영체제(OS) -> 프로세스(Process) -> 스레드(Thread) 순으로 이어지는 계층적 관리 및 실행 구조를 가짐.

2. 멀티 프로세스 모델의 제약 및 스레드 특징

멀티 프로세스 모델의 제약 사항

  • 자원 점유: 프로세스 생성 및 관리에 따른 시스템 리소스 소모량 높음.
  • Context Switch 오버헤드: 프로세스 제어 블록(PCB) 정보의 저장 및 복원 과정에서 CPU 부하 수반.
  • 데이터 공유 한계: 독립적 주소 공간 운영으로 인해 IPC 기법 도입 필수 및 통신 비용 발생.

스레드(Thread)의 기술적 이점

  • 실행 효율성: 프로세스 대비 경량화된 단위로 Context Switch 속도 향상.
  • 자원 공유: 프로세스 내 DataHeap 영역을 공유하여 전역 변수 등에 직접 접근 가능.
  • 통신 오버헤드 감소: 별도 IPC나 파이프 없이 스레드 간 효율적인 데이터 교환 수행.
  • 병렬 실행 흐름: 단일 프로세스 내 복수의 실행 흐름(Execution Flow)을 생성하여 처리량 증대.

프로세스 vs 스레드 비교

  • 실행 단위: 프로세스는 운영체제 수준의 독립적 실행 단위이며, 스레드는 프로세스 내부에 종속된 실행 흐름임.
  • 메모리 구조:
    • 프로세스: Code, Data, Stack, Heap 영역을 독립적으로 할당 및 운영함.
    • 스레드: Stack 영역만 개별 할당하며, 그 외 영역은 부모 프로세스의 자원을 공유함.

3. 스레드 문맥 교환 및 스케줄링

문맥 교환(Context Switch) 주체

  • OS 커널 직접 관리: 현대 리눅스(NPTL) 환경에서 스레드는 커널 수준 스레드(LWP)와 1:1 매핑되므로, 실제 문맥 교환의 결정 및 수행은 운영체제 커널 스케줄러가 담당함.
  • pthread 역할: 라이브러리 차원에서 교환을 직접 수행하지 않으며, 커널 스케줄러에 정책 및 우선순위를 전달하는 인터페이스 역할을 수행함.

주요 스케줄링 정책 (Scheduling Policy)

  • SCHED_OTHER (기본값): 완전 공평 스케줄러(CFS) 기반의 시분할 방식. 정적 우선순위가 아닌 시스템 부하 및 가중치(nice)에 따른 공평한 자원 배분.
  • SCHED_FIFO / SCHED_RR: 실시간 정책으로, 명시적인 우선순위 설정이 가능함 (루트 권한 필요).

우선순위 결정 방식

  • 상속 (Inheritance): 기본적으로 부모 스레드의 정책과 우선순위를 상속받음 (PTHREAD_INHERIT_SCHED).
  • 직접 지정: pthread_attr_t 객체를 초기화하여 생성 시점에 정책 및 우선순위 값을 명시적으로 부여 가능함 (PTHREAD_EXPLICIT_SCHED).

4. POSIX Thread 실행 체계 및 예제

메인 스레드 (Main Thread) 특성

  • 최초 실행 흐름: 프로세스 생성 시 운영체제에 의해 자동 생성되는 첫 번째 스레드.
  • 프로세스 수명 제어: 메인 스레드 종료 시 프로세스 내 모든 하위 스레드 강제 종료됨.

pthread_create 함수 인터페이스

  • pthread_t *thread: 생성된 스레드의 식별값(ID)을 저장할 변수의 주소.
  • const pthread_attr_t *attr: 스레드 속성 지정 (기본 설정 시 NULL).
  • void *(*start_routine)(void *): 스레드가 실행할 함수(Main Routine) 포인터.
  • void *arg: 실행 함수로 전달할 매개변수 주소 (다중 인자 전달 시 구조체 활용).

구현 예제 (기본 생성 및 실행)

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

void* thread_main(void *arg) {
    int cnt = *((int*)arg);
    for (int i = 0; i < cnt; i++) {
        sleep(1);
        puts("running thread");
    }
    return NULL;
}

int main() {
    pthread_t t_id;
    int thread_param = 5;

    // 스레드 생성 수행
    if (pthread_create(&t_id, NULL, thread_main, (void*)&thread_param) != 0) {
        perror("pthread_create error");
        return -1;
    }

    sleep(10); // 실행 흐름 유지를 위한 지연
    return 0;
}

스레드 종료 및 자원 회수

  • 자원 소멸 특성: 스레드 함수가 반환(return)되더라도 시스템 자원이 자동으로 완전히 소멸되지 않으므로, 명시적인 회수 절차가 필요함.
  • pthread_join (블로킹 방식): 대상 스레드가 종료될 때까지 호출부(주로 메인 스레드)를 블로킹하며, 종료 후 자원을 회수함. 다른 작업을 병행할 수 없음.
  • pthread_detach (비블로킹 방식): 스레드를 분리 상태로 설정하여, 스레드 종료 시 별도의 대기 없이 운영체제가 즉시 자원을 회수하도록 함. 호출부는 블로킹되지 않고 즉시 다음 코드를 수행함.

컴파일 및 빌드

  • 빌드 커맨드: gcc thread.c -o thread -lpthread
  • 전처리기 지침 (#elif): #ifdef#endif 사이에서 다중 조건을 판별할 때 사용하며, #elif defined(MACRO) 형식을 권장함.
  • 기술 노트 (포인터 크기): 포인터 변수의 크기는 아키텍처(32bit: 4byte, 64bit: 8byte)에 따라 결정되며, void*를 통한 인자 전달 시 시스템 아키텍처를 고려해야 함.

5. 스레드 자원 공유 및 동기화 전략

함수의 공용 실행 원리

  • 코드 영역 공유: 프로세스 내 모든 스레드는 동일한 Code 영역을 공유하므로 단일 함수를 동시 실행 가능함.
  • 스택 분리 기반 독립성: 각 스레드는 고유한 Stack 영역을 할당받아 지역 변수를 개별 관리함.
  • 재진입성(Re-entrancy): 지역 변수만 사용하여 동시 호출 시에도 안전하게 동작하는 특성.

뮤텍스(Mutex) 락 범위 설정

  • 광범위한 락 (Coarse-grained): 루프 전체를 잠금. 오버헤드 적으나 동시성 저하.
  • 세밀한 락 (Fine-grained): 연산 단위 잠금. 동시성 높으나 시스템 콜 오버헤드 증가.
  • 권장: 임계 구역은 최소화하되, 동기화 함수 호출 비용과의 균형을 고려하여 설계.

구현 예시: 재진입 가능 함수 vs 뮤텍스 동기화

// 재진입 가능: 지역 변수만 활용
int add_reentrant(int a, int b) {
    int result = a + b; 
    return result;
}

// 뮤텍스 동기화: 전역 자원 보호
int global_sum = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void add_with_mutex(int value) {
    pthread_mutex_lock(&mutex);   
    global_sum += value;          
    pthread_mutex_unlock(&mutex); 
}

6. 세마포어(Semaphore)

인터페이스 명세

  • sem_init(sem, pshared, value): 세마포어 초기화. pshared가 0이면 단일 프로세스 내 공유, value는 초기 카운트 값임.
  • sem_destroy(sem): 할당된 세마포어 자원 해제.
  • sem_post(sem): 세마포어 값을 1 증가시킴 (신호 송신).
  • sem_wait(sem): 세마포어 값을 1 감소시킴. 값이 0인 경우 양수가 될 때까지 대기(블로킹).

실행 시나리오: 스레드 간 실행 순서 제어 (read & accu)

두 개의 세마포어(sem_one: 초기값 0, sem_two: 초기값 1)를 사용하여 입력과 연산의 순차적 실행을 보장함.

  1. read 스레드 (입력 담당):
    • 초기 상태에서 sem_two가 1이므로 sem_wait(&sem_two)를 즉시 통과하여 입력을 수행함.
    • 입력 완료 후 sem_post(&sem_one)을 호출하여 연산 스레드에 신호를 보냄.
sem_wait(&sem_two); // 입력 권한 획득 (1 -> 0)
scanf("%d", &num);
sem_post(&sem_one); // 연산 스레드에 신호 송신 (0 -> 1)
  1. accu 스레드 (연산 담당):
    • 초기 상태에서 sem_one이 0이므로 sem_wait(&sem_one)에서 대기함.
    • read 스레드로부터 신호를 받으면 연산을 수행하고 sem_post(&sem_two)를 호출하여 다음 입력을 허용함.
sem_wait(&sem_one); // 연산 권한 대기 (1 -> 0)
sum += num;
sem_post(&sem_two); // 입력 스레드에 신호 송신 (0 -> 1)

7. 스레드 안전(Thread-safe) 함수 및 설계

  • 구분 기준: 동시 호출 시 공유 자원(전역/정적 변수) 오염 여부에 따라 결정됨.
  • _REENTRANT 매크로: 표준 라이브러리 함수를 스레드 안전 버전(예: _r 버전)으로 자동 대체 및 errno 등의 스레드 개별화 수행.
  • 워커 스레드 모델: 메인 스레드가 작업을 분배하고 하위 워커 스레드들이 병렬 연산을 수행하는 패턴. 임계 구역을 통한 결과 취합이 필수적임.

8. 스레드 모니터링 (CLI)

  • ps -efT | grep [Keyword] | wc -l: 실행 중인 스레드 개수 실시간 확인.
  • 숫자가 감소하는 것은 스레드가 작업을 마치고 정상 종료되었음을 의미함.

[새로운내용]