스레드 안전 함수 및 라이브러리 (Thread-Safe Function & Library)

Ⅰ. 개요

1. 정의

스레드 안전(Thread-Safe) 함수란 여러 스레드가 동시에 호출하더라도 올바른 결과를 반환하는 함수를 의미한다. 멀티스레드 환경에서 공유 자원에 대한 동시 접근 문제를 해결해야 한다.

스레드 안전의 핵심 원칙
┌─────────────────────────────────────────────────┐
│                                                 │
│   Thread A ──→ [함수 F()] ──→ 공유 자원        │
│                    │                            │
│              뮤텍스/세마포어                     │
│                    │                            │
│   Thread B ──→ [함수 F()] ──→ 공유 자원        │
│                                                 │
│   ★ 동시에 실행되어도 데이터 일관성 보장          │
└─────────────────────────────────────────────────┘

2. 스레드 안전 vs 재진입 가능

┌──────────────┐         ┌──────────────────┐
│ Thread-Safe  │         │   Reentrant      │
├──────────────┤         ├──────────────────┤
│ 동기화 사용  │         │  동기화 불필요   │
│ 락으로 보호  │    ⊇   │ 공유 데이터 없음 │
│ 느릴 수 있음 │         │ 항상 빠름        │
│              │         │ 더 엄격한 조건   │
└──────────────┘         └──────────────────┘
    재진입 가능 = 스레드 안전
    스레드 안전 ≠ 재진입 가능

비유: 스레드 안전은 "화장실에 열쇠가 있어서 한 번에 한 명만 들어갈 수 있는 것"이고, 재진입 가능은 "각자 자기 집 화장실이 있어서 기다릴 필요가 없는 것"이다.

Ⅱ. 스레드 안전의 세 가지 조건

1. 상호 배제 (Mutual Exclusion)

공유 데이터에 대한 접근을 하나의 스레드만 허용한다.

상호 배제 동작 원리
┌─────────┐     lock()      ┌─────────────┐
│Thread A │ ──────────────→ │ Critical    │
└─────────┘                  │ Section     │
                             │ (공유 데이터)│
┌─────────┐     대기...      │             │
│Thread B │ ──────────────→ │             │
└─────────┘     unlock()    │             │
                ←──────────  │             │
┌─────────┘     lock()      │             │
│Thread B │ ──────────────→ │             │
└─────────┘                  └─────────────┘

2. 원자적 연산 (Atomic Operations)

연산이 중간에 인터럽트되지 않고 하나의 단위로 실행된다.

  • CAS (Compare-And-Swap): 하드웨어 수준의 원자적 연산
  • fetch_add, fetch_sub: 원자적 증가/감소 연산
  • GCC 내장 함수: __atomic_*, __sync_*

3. 불변 데이터 (Immutable Data)

공유 데이터가 읽기 전용(immutable)이면 동기화가 필요 없다.

불변 데이터의 장점
┌──────────────────────────────────────────┐
│  초기화 후 변경 불가 (const)              │
│                                          │
│  Thread A ──→ [읽기] ──→ 같은 값         │
│  Thread B ──→ [읽기] ──→ 같은 값         │
│  Thread C ──→ [읽기] ──→ 같은 값         │
│                                          │
│  ★ 동기화 오버헤드 제로                   │
└──────────────────────────────────────────┘

Ⅲ. 스레드 안전 vs 재진입 가능 비교

1. 스레드 안전 함수 (Thread-Safe but NOT Reentrant)

스레드 안전하되 재진입 불가한 예시
┌────────────────────────────────────────────────┐
│  int counter = 0;                             │
│                                                │
│  int safe_increment() {                        │
│      mutex_lock(&m);    // 락 획득             │
│      counter++;        // 공유 데이터 수정      │
│      mutex_unlock(&m);  // 락 해제             │
│      return counter;                            │
│  }                                             │
│                                                │
│  문제점:                                       │
│  - 같은 스레드가 시그널로 중단되면               │
│    락을 다시 획득하려 시도 → 교착 상태!         │
└────────────────────────────────────────────────┘

2. 재진입 가능 함수 (Reentrant)

재진입 가능한 예시
┌────────────────────────────────────────────────┐
│  int reentrant_increment(int *counter) {       │
│      return (*counter)++;  // 로컬 포인터 사용  │
│  }                                             │
│                                                │
│  특징:                                         │
│  - 정적/전역 변수 없음                          │
│  - 모든 데이터를 호출자로부터 전달받음           │
│  - 동기화 불필요 (자연스럽게 안전)              │
│  - 시그널 핸들러에서도 안전하게 호출 가능       │
└────────────────────────────────────────────────┘

Ⅳ. 표준 라이브러리 예시

1. strtok_r() vs strtok()

strtok() - 스레드 안전하지 않음 (내부 정적 버퍼 사용)
┌───────────────────────────────────────────────────┐
│  Thread A: strtok(str1, ",") ──→ 내부 버퍼 설정   │
│  Thread B: strtok(str2, ",") ──→ 내부 버퍼 덮어씀! │
│  Thread A: strtok(NULL, ",")  ──→ 잘못된 결과!     │
└───────────────────────────────────────────────────┘

strtok_r() - 재진입 가능 (상태를 호출자가 관리)
┌───────────────────────────────────────────────────┐
│  Thread A:                                        │
│    char *saveptr_a;                                │
│    strtok_r(str1, ",", &saveptr_a);  // 독립 상태  │
│                                                   │
│  Thread B:                                        │
│    char *saveptr_b;                                │
│    strtok_r(str2, ",", &saveptr_b);  // 독립 상태  │
│                                                   │
│  ★ 각 스레드가 자신의 saveptr를 관리               │
└───────────────────────────────────────────────────┘

2. localtime_r() vs localtime()

함수스레드 안전재진입 가능상태 저장
localtime()XX정적 버퍼
localtime_r()OO호출자 버퍼
gmtime()XX정적 버퍼
gmtime_r()OO호출자 버퍼
ctime()XX정적 버퍼
ctime_r()OO호출자 버퍼

Ⅴ. 지식 그래프 및 요약

1. 지식 그래프

[스레드 안전]
├── [상호 배제] ─── 뮤텍스, 세마포어, 스핀락
├── [원자적 연산] ── CAS, fetch_add, __atomic_*
├── [불변 데이터] ── const, 읽기 전용
├── [_r 함수들] ─── strtok_r, localtime_r, asctime_r
└── [라이브러리] ── POSIX 스레드 안전 함수 규격

[재진입 가능]
├── [특징] ────── 정적 변수 없음, 호출자 데이터만 사용
├── [순수 함수] ─── 입력만으로 출력 결정
└── [용도] ────── 시그널 핸들러, 인터럽트 서비스 루틴

2. 준말

  • TSF: Thread-Safe Function (스레드 안전 함수)
  • CAS: Compare-And-Swap (비교 후 교체)

3. 어린이를 위한 3줄 설명

여러 사람이 동시에 하나의 장난감을 가지고 놀면 싸우게 돼요. 그래서 번호표를 뽑아 순서대로 놀게 하는 것이 스레드 안전이에요. 각자 자기 장난감을 가지고 놀면 기다릴 필요가 없는데, 이게 더 좋은 방법인 재진입 가능이에요.