C언어에서 멀티스레드 프로그래밍은 병렬 처리 성능을 극대화하기 위해 필수적인 기술입니다. 하지만 스레드 간의 자원 공유는 의도하지 않은 충돌과 불안정성을 초래할 수 있습니다. 이러한 문제를 예방하기 위해 스레드 안전한 코드 작성이 중요합니다. 본 기사에서는 스레드 안전의 기본 개념부터 동기화 기법, 라이브러리 활용, 디버깅 방법까지 다루며, 실무에서 적용 가능한 예제와 연습 문제를 통해 멀티스레드 환경에서 안정적이고 신뢰할 수 있는 코드를 작성하는 방법을 제시합니다.
스레드 안전의 기본 개념
멀티스레드 프로그래밍에서 스레드 안전(Thread-Safe)이란, 여러 스레드가 동시에 동일한 코드를 실행해도 프로그램의 동작이 예기치 않게 변경되지 않는 상태를 의미합니다. 스레드 간의 데이터 충돌을 방지하고, 정확하고 일관된 결과를 보장하는 것이 핵심입니다.
스레드 안전이 필요한 이유
멀티스레드 환경에서는 여러 스레드가 동일한 메모리 공간을 공유합니다. 이로 인해, 동기화되지 않은 데이터 접근은 다음과 같은 문제를 발생시킬 수 있습니다:
- 데드락(Deadlock): 두 개 이상의 스레드가 서로 자원을 기다리며 멈추는 상태.
- 레이스 컨디션(Race Condition): 여러 스레드가 동시에 데이터를 변경하면서 예기치 않은 결과를 초래하는 상태.
- 데이터 손상(Data Corruption): 스레드 간 비동기 접근으로 인해 데이터가 손상됨.
스레드 안전 보장 원칙
스레드 안전을 보장하려면 다음의 기본 원칙을 따라야 합니다:
- 공유 자원 최소화: 가능한 한 데이터를 각 스레드에 국한시켜 공유를 줄입니다.
- 동기화 도구 사용: 뮤텍스, 세마포어 등 적절한 동기화 도구를 활용합니다.
- 원자적 연산 사용: 데이터 변경 작업을 원자적으로 처리하여 충돌을 방지합니다.
예시: 스레드 안전과 비안전 코드
#include <stdio.h>
#include <pthread.h>
int counter = 0; // 공유 자원
void* increment() {
for (int i = 0; i < 1000000; i++) {
counter++; // 비스레드 안전 코드
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final Counter: %d\n", counter); // 예상치 않은 결과
return 0;
}
위 코드에서 counter
변수는 두 스레드에서 동시에 접근되어 레이스 컨디션을 초래합니다. 다음 항목에서는 이러한 문제를 해결하는 다양한 동기화 기법을 다룹니다.
동기화 기법
동기화 기법은 멀티스레드 환경에서 공유 자원 접근을 제어하여 데이터 충돌과 같은 문제를 방지하는 핵심 도구입니다. 적절한 동기화 기법을 선택하면 프로그램의 안정성과 성능을 동시에 확보할 수 있습니다.
뮤텍스(Mutex)
뮤텍스는 Mutual Exclusion의 약자로, 단일 스레드만 특정 코드 블록이나 자원에 접근할 수 있도록 보장합니다.
- 특징:
- 한 번에 하나의 스레드만 잠금을 소유할 수 있음.
- 자원의 독점적 사용을 보장함.
- 예제:
#include <stdio.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t lock;
void* increment() {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock); // 잠금
counter++;
pthread_mutex_unlock(&lock); // 잠금 해제
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&lock, NULL);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock);
printf("Final Counter: %d\n", counter); // 예상한 결과
return 0;
}
세마포어(Semaphore)
세마포어는 자원을 제한된 개수만큼 동시 접근 가능하도록 제어합니다.
- 특징:
- 주로 여러 스레드가 제한된 자원을 공유할 때 사용.
- 값을 1로 설정하면 뮤텍스와 비슷하게 작동.
- 예제:
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int counter = 0;
sem_t sem;
void* increment() {
for (int i = 0; i < 1000000; i++) {
sem_wait(&sem); // 잠금
counter++;
sem_post(&sem); // 잠금 해제
}
return NULL;
}
int main() {
pthread_t t1, t2;
sem_init(&sem, 0, 1);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sem);
printf("Final Counter: %d\n", counter); // 예상한 결과
return 0;
}
스핀락(Spinlock)
스핀락은 잠금이 해제될 때까지 바쁜 대기 상태를 유지하는 기법입니다. 성능이 중요한 경우에 사용됩니다.
기법 선택 기준
- 뮤텍스: 자원 접근이 드물거나 여러 자원을 보호해야 하는 경우 적합.
- 세마포어: 제한된 공유 자원을 여러 스레드가 사용할 때 적합.
- 스핀락: 짧은 대기 시간이 예상될 때 고성능 용도로 적합.
적절한 동기화 기법을 사용하면 스레드 간 데이터 충돌을 예방하고 프로그램의 안정성을 높일 수 있습니다. 다음 항목에서는 재진입 가능 코드 작성 방법을 다룹니다.
재진입 가능 코드와 스레드 안전
재진입 가능 코드(Reentrant Code)는 스레드 안전성을 구현하는 중요한 기법 중 하나입니다. 재진입 가능 코드는 여러 스레드가 동시에 실행해도 상태 공유 없이 독립적으로 동작할 수 있는 코드를 의미합니다.
재진입 가능 코드의 특징
- 공유 데이터 사용 금지: 전역 변수와 정적 변수 사용을 피하고, 함수 내에서만 데이터를 유지.
- 순수 함수 작성: 입력값만을 기반으로 결과를 반환하고, 외부 상태에 의존하지 않음.
- 스택 기반 데이터: 데이터는 함수 호출 스택에 저장되어 각 스레드가 독립적으로 사용 가능.
재진입 가능 코드 예제
다음은 재진입 가능 코드와 비재진입 가능 코드를 비교한 예제입니다:
비재진입 가능 코드
#include <stdio.h>
char buffer[100]; // 전역 변수 사용
const char* format_message(const char* message) {
snprintf(buffer, sizeof(buffer), "Message: %s", message);
return buffer; // 전역 변수를 반환
}
void print_messages() {
const char* msg1 = format_message("Hello");
const char* msg2 = format_message("World");
printf("%s\n", msg1); // 결과가 의도와 다를 수 있음
printf("%s\n", msg2);
}
재진입 가능 코드
#include <stdio.h>
#include <string.h>
char* format_message(const char* message, char* buffer, size_t size) {
snprintf(buffer, size, "Message: %s", message);
return buffer; // 함수 호출 스택의 버퍼 사용
}
void print_messages() {
char buffer1[100];
char buffer2[100];
const char* msg1 = format_message("Hello", buffer1, sizeof(buffer1));
const char* msg2 = format_message("World", buffer2, sizeof(buffer2));
printf("%s\n", msg1); // 독립적인 결과 보장
printf("%s\n", msg2);
}
재진입 가능 코드 작성의 장점
- 스레드 안전성 확보: 데이터 충돌 없이 여러 스레드에서 안전하게 호출 가능.
- 예측 가능성: 외부 상태에 의존하지 않아 테스트와 디버깅이 용이.
- 이식성 향상: 다양한 환경에서 안정적인 작동 보장.
재진입 가능 코드 작성 시 주의점
- 전역 변수, 정적 변수 사용 자제.
- 동적 메모리 할당 최소화 및 관리 철저.
- 외부 라이브러리 호출 시 해당 함수가 재진입 가능한지 확인.
재진입 가능 코드는 스레드 안전성을 보장하는 기본적인 기법이며, 다른 동기화 기법과 병행해 사용하면 보다 안전한 멀티스레드 코드를 작성할 수 있습니다. 다음 항목에서는 스레드 로컬 저장소를 활용하는 방법을 다룹니다.
스레드 로컬 저장소 활용
스레드 로컬 저장소(Thread Local Storage, TLS)는 각 스레드가 독립적으로 데이터를 저장하고 사용할 수 있도록 하는 기법입니다. 스레드 로컬 변수를 사용하면 공유 자원 없이 스레드 간 데이터 충돌을 방지할 수 있습니다.
스레드 로컬 저장소의 특징
- 스레드별 독립 데이터: 각 스레드가 자체적으로 데이터를 보유하므로 다른 스레드에 영향을 미치지 않음.
- 동기화 불필요: 스레드별 데이터로 동기화 기법 없이 안전하게 사용 가능.
- 사용 용도: 스레드별 상태 저장, 스레드 간 독립 데이터 처리.
스레드 로컬 저장소 예제
C언어에서 __thread
또는 C11의 _Thread_local
키워드를 사용하여 스레드 로컬 변수를 선언할 수 있습니다.
POSIX 스레드에서의 TLS 예제
#include <stdio.h>
#include <pthread.h>
pthread_key_t key; // 스레드별 키 생성
void destructor(void* value) {
free(value); // 메모리 해제
}
void* thread_function(void* arg) {
char* tls_data = (char*)malloc(100);
snprintf(tls_data, 100, "Thread %ld's data", (long)arg);
pthread_setspecific(key, tls_data); // 스레드별 데이터 저장
printf("%s\n", (char*)pthread_getspecific(key)); // 스레드별 데이터 출력
return NULL;
}
int main() {
pthread_t threads[3];
pthread_key_create(&key, destructor); // 키 초기화
for (long i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, thread_function, (void*)i);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_key_delete(key); // 키 삭제
return 0;
}
C11 _Thread_local
키워드 사용 예제
#include <stdio.h>
#include <threads.h>
_Thread_local int thread_local_data = 0;
int thread_function(void* arg) {
thread_local_data = *(int*)arg;
printf("Thread %d: thread_local_data = %d\n", thrd_current(), thread_local_data);
return 0;
}
int main() {
thrd_t threads[3];
int values[] = {1, 2, 3};
for (int i = 0; i < 3; i++) {
thrd_create(&threads[i], thread_function, &values[i]);
}
for (int i = 0; i < 3; i++) {
thrd_join(threads[i], NULL);
}
return 0;
}
스레드 로컬 저장소의 장점
- 데이터 충돌 방지: 스레드별 데이터 격리로 충돌 문제를 원천적으로 차단.
- 코드 간결화: 동기화 코드 없이 멀티스레드 환경 구현 가능.
- 성능 최적화: 공유 메모리 동기화에 따른 성능 저하를 방지.
활용 사례
- 스레드별 로깅: 각 스레드가 자체 로깅 데이터를 보유하여 독립적인 기록 관리.
- 스레드별 상태 유지: 데이터베이스 연결 정보, 임시 계산값 등 상태 정보를 스레드 단위로 저장.
스레드 로컬 저장소는 스레드 간 독립성을 유지하면서 성능을 최적화하는 강력한 도구입니다. 다음 항목에서는 원자적 연산과 메모리 모델에 대해 설명합니다.
원자적 연산과 메모리 모델
원자적 연산(Atomic Operation)은 분할되지 않는 단일 작업으로 실행되며, 멀티스레드 환경에서 동기화 없이도 데이터 일관성을 보장합니다. C언어에서 원자적 연산은 스레드 안전성을 유지하는 데 중요한 도구로 활용됩니다.
원자적 연산의 특징
- 중단 불가능성: 작업 도중 다른 스레드가 개입하지 못함.
- 동기화 불필요: 별도의 락 없이도 스레드 간 데이터 일관성을 유지.
- 효율성: 동기화 도구에 비해 오버헤드가 낮음.
C언어에서의 원자적 연산
C11 표준에서 원자적 연산은 <stdatomic.h>
헤더를 통해 지원됩니다. 이를 사용하면 CPU 수준에서 제공하는 원자적 명령어를 활용할 수 있습니다.
원자적 데이터 타입 정의
#include <stdatomic.h>
atomic_int counter = 0; // 원자적 정수
원자적 연산 사용 예제
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
atomic_int counter = 0; // 원자적 변수
void* increment() {
for (int i = 0; i < 1000000; i++) {
atomic_fetch_add(&counter, 1); // 원자적 증가
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final Counter: %d\n", counter); // 정확한 결과 보장
return 0;
}
C언어 메모리 모델
C11에서는 메모리 모델을 도입하여 스레드 간 메모리 접근 규칙을 명시했습니다.
- 순서 제어:
memory_order
열거형을 사용하여 메모리 접근 순서를 제어 가능. memory_order_relaxed
: 순서 제약 없이 빠른 동작 보장.memory_order_acquire
: 이후 연산이 이전 연산 결과에 의존하도록 보장.memory_order_release
: 이전 연산이 이후 연산에 선행하도록 보장.memory_order_seq_cst
: 가장 강력한 순서 보장.
메모리 모델 사용 예제
#include <stdatomic.h>
#include <stdio.h>
#include <pthread.h>
atomic_int flag = 0;
void* writer(void* arg) {
atomic_store_explicit(&flag, 1, memory_order_release); // 데이터 쓰기 완료 후 플래그 설정
return NULL;
}
void* reader(void* arg) {
while (!atomic_load_explicit(&flag, memory_order_acquire)) {
// 플래그 값이 1이 될 때까지 대기
}
printf("Data ready!\n");
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, writer, NULL);
pthread_create(&t2, NULL, reader, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
원자적 연산과 메모리 모델의 활용 사례
- 카운터 및 플래그 관리: 스레드 간 데이터 충돌 없이 상태를 관리.
- 락 프리 알고리즘: 고성능 환경에서 락 없는 데이터 구조 구현.
- 비동기 이벤트 처리: 스레드 간 이벤트 신호 전달.
장단점
- 장점: 낮은 오버헤드, 간결한 코드, 높은 성능.
- 단점: 복잡한 순서 제어 및 제한된 표현력.
원자적 연산과 메모리 모델은 고성능 멀티스레드 프로그래밍에서 필수적인 도구입니다. 다음 항목에서는 스레드 안전 라이브러리 활용 방법을 다룹니다.
스레드 안전 라이브러리 활용
스레드 안전한 코드 작성을 위해 다양한 라이브러리를 활용하면 반복적인 구현을 줄이고 안정성을 높일 수 있습니다. C언어에서는 POSIX 스레드(Pthreads)와 C11 스레드 라이브러리가 널리 사용됩니다.
POSIX 스레드(Pthreads)
POSIX 스레드는 다양한 플랫폼에서 사용할 수 있는 표준화된 스레드 라이브러리입니다. 주요 기능으로 스레드 생성, 동기화, 자원 관리를 제공합니다.
Pthreads 주요 API
pthread_create
: 새로운 스레드 생성.pthread_join
: 스레드 완료 대기.pthread_mutex_*
: 뮤텍스 동기화 관련 함수.pthread_cond_*
: 조건 변수 관련 함수.
Pthreads 활용 예제
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t lock;
int counter = 0;
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock); // 뮤텍스 잠금
counter++;
pthread_mutex_unlock(&lock); // 뮤텍스 해제
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&lock, NULL);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock);
printf("Final Counter: %d\n", counter);
return 0;
}
C11 스레드 라이브러리
C11 표준은 스레드와 동기화를 위한 기능을 내장하여, 보다 간단하고 표준화된 방식으로 스레드 안전 코드를 작성할 수 있습니다.
C11 주요 API
thrd_create
: 새로운 스레드 생성.thrd_join
: 스레드 완료 대기.mtx_*
: 뮤텍스 동기화 관련 함수.cnd_*
: 조건 변수 관련 함수.
C11 활용 예제
#include <stdio.h>
#include <threads.h>
mtx_t lock;
int counter = 0;
int increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
mtx_lock(&lock); // 뮤텍스 잠금
counter++;
mtx_unlock(&lock); // 뮤텍스 해제
}
return 0;
}
int main() {
thrd_t t1, t2;
mtx_init(&lock, mtx_plain);
thrd_create(&t1, increment, NULL);
thrd_create(&t2, increment, NULL);
thrd_join(t1, NULL);
thrd_join(t2, NULL);
mtx_destroy(&lock);
printf("Final Counter: %d\n", counter);
return 0;
}
POSIX와 C11 비교
- 호환성: POSIX는 다양한 플랫폼에서 지원, C11은 최신 컴파일러에서만 지원.
- 사용 편의성: C11은 간결한 코드 작성 가능, POSIX는 세부적인 제어 제공.
- 표준화: C11은 C언어 표준의 일부, POSIX는 표준화된 확장이지만 독립적.
라이브러리 활용의 장점
- 생산성 향상: 반복적인 구현 없이 안정적인 코드 작성.
- 디버깅 간소화: 검증된 동기화 도구를 사용해 오류 감소.
- 이식성 증가: 표준화된 API로 플랫폼 간 호환성 확보.
적절한 라이브러리를 활용하면 스레드 안전한 코드를 더 쉽게 작성할 수 있습니다. 다음 항목에서는 스레드 안전 문제의 디버깅 방법을 다룹니다.
스레드 안전 문제의 디버깅
스레드 안전 문제는 동시성 관련 버그로 인해 발생하며, 디버깅이 까다롭습니다. 대표적인 문제로 데드락, 레이스 컨디션, 데이터 손상이 있습니다. 이러한 문제를 식별하고 해결하기 위한 체계적인 접근법과 도구를 알아봅니다.
대표적인 스레드 안전 문제
데드락(Deadlock)
두 개 이상의 스레드가 서로의 자원을 기다리며 무한 대기 상태에 빠지는 현상입니다.
- 원인: 잘못된 자원 잠금 순서.
- 해결:
- 항상 자원을 고정된 순서로 잠금.
- 타임아웃을 설정하여 무한 대기 방지.
레이스 컨디션(Race Condition)
두 개 이상의 스레드가 동시 접근하여 데이터가 예상치 못한 상태로 변경되는 현상입니다.
- 원인: 동기화 없이 공유 데이터 접근.
- 해결:
- 뮤텍스 또는 세마포어로 동기화.
- 원자적 연산 사용.
데이터 손상(Data Corruption)
스레드 간 비동기적 데이터 접근으로 인해 잘못된 값이 저장되는 문제입니다.
- 원인: 동기화 없이 데이터 수정.
- 해결:
- 동기화 도구를 통해 데이터 접근 관리.
- 재진입 가능 코드 작성.
디버깅 전략
문제 재현
- 테스트 환경 구축: 문제가 자주 발생하도록 스레드 수와 데이터 부하를 조정.
- 로그 삽입: 문제 발생 지점을 확인하기 위해 상세 로그 추가.
스레드 디버거 사용
- gdb: GNU 디버거는 멀티스레드 환경에서 실행 중인 스레드의 상태를 분석 가능.
- Valgrind: 멀티스레드 관련 문제를 식별하는 Memcheck와 Helgrind 도구를 제공.
Valgrind Helgrind 예제
valgrind --tool=helgrind ./your_program
디버깅 도구 활용
- ThreadSanitizer(TSan): 구글에서 제공하는 스레드 디버깅 도구로 레이스 컨디션 탐지 가능.
- 사용법: 프로그램 컴파일 시
-fsanitize=thread
플래그 추가. - 예제:
bash gcc -fsanitize=thread -o program program.c ./program
문제 해결 사례
- 데드락 해결
자원 잠금 순서를 고정하여 데드락 방지.
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2);
// 작업 수행
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
- 레이스 컨디션 해결
원자적 연산으로 데이터 충돌 방지.
atomic_fetch_add(&counter, 1);
디버깅 체크리스트
- 공유 데이터 접근은 모두 동기화 되었는가?
- 자원 잠금 순서가 일관적인가?
- 재진입 가능 코드로 작성되었는가?
- 동기화 도구가 적절히 사용되었는가?
결론
스레드 안전 문제의 디버깅은 체계적인 접근과 적절한 도구의 활용이 핵심입니다. 문제를 조기에 식별하고 해결하는 방법을 익히면 멀티스레드 프로그램의 안정성과 성능을 크게 향상시킬 수 있습니다. 다음 항목에서는 스레드 안전한 코드 작성 연습을 다룹니다.
스레드 안전한 코드 작성 연습
스레드 안전한 코드를 작성하려면 기본 원칙과 기법을 실습을 통해 체득하는 것이 중요합니다. 아래는 다양한 사례와 연습 문제를 통해 실전 감각을 익힐 수 있는 방법을 소개합니다.
사례 1: 공유 데이터의 안전한 증가
공유 데이터를 여러 스레드가 동시에 증가시키는 상황에서 뮤텍스를 활용해 안전하게 구현하는 연습입니다.
문제: 아래 코드를 스레드 안전하게 수정하세요.
#include <stdio.h>
#include <pthread.h>
int counter = 0;
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
counter++; // 비스레드 안전 코드
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final Counter: %d\n", counter);
return 0;
}
해답: 뮤텍스 추가
#include <stdio.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t lock;
void* increment(void* arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&lock, NULL);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock);
printf("Final Counter: %d\n", counter);
return 0;
}
사례 2: 조건 변수 사용
생산자-소비자 문제를 해결하며 스레드 안전한 코드를 작성합니다.
문제: 버퍼가 가득 차거나 비었을 때 안전하게 생산자와 소비자를 동기화하세요.
해답:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;
pthread_mutex_t lock;
pthread_cond_t not_empty, not_full;
void* producer(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&lock);
while (count == BUFFER_SIZE) {
pthread_cond_wait(¬_full, &lock);
}
buffer[count++] = i;
printf("Produced: %d\n", i);
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&lock);
sleep(1);
}
return NULL;
}
void* consumer(void* arg) {
for (int i = 0; i < 10; i++) {
pthread_mutex_lock(&lock);
while (count == 0) {
pthread_cond_wait(¬_empty, &lock);
}
int item = buffer[--count];
printf("Consumed: %d\n", item);
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&lock);
sleep(1);
}
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_mutex_init(&lock, NULL);
pthread_cond_init(¬_empty, NULL);
pthread_cond_init(¬_full, NULL);
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(¬_empty);
pthread_cond_destroy(¬_full);
return 0;
}
사례 3: 원자적 연산으로 상태 관리
원자적 연산을 사용하여 스레드 안전성을 확보하는 연습입니다.
문제: 아래 코드에서 동기화 문제를 해결하세요.
#include <stdio.h>
#include <pthread.h>
int flag = 0;
void* set_flag(void* arg) {
flag = 1;
return NULL;
}
void* check_flag(void* arg) {
while (!flag);
printf("Flag set!\n");
return NULL;
}
해답: 원자적 연산 추가
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
atomic_int flag = 0;
void* set_flag(void* arg) {
atomic_store(&flag, 1);
return NULL;
}
void* check_flag(void* arg) {
while (!atomic_load(&flag));
printf("Flag set!\n");
return NULL;
}
연습의 효과
- 동기화 문제를 스스로 발견하고 해결할 수 있는 능력 향상.
- 다양한 기법을 실제 코드에 적용하는 실전 감각 강화.
- 스레드 안전한 프로그램 작성에 대한 자신감 배양.
연습 문제를 반복하며 스레드 안전성을 확보하는 다양한 방법을 익히세요. 다음 항목에서는 전체 내용을 요약합니다.
요약
C언어에서 스레드 안전한 코드는 멀티스레드 프로그래밍의 성공을 결정짓는 중요한 요소입니다. 본 기사에서는 스레드 안전의 기본 개념, 동기화 기법, 재진입 가능 코드 작성, 스레드 로컬 저장소 활용, 원자적 연산과 메모리 모델, 스레드 안전 라이브러리 사용법, 디버깅 방법, 그리고 실습 예제를 다루었습니다.
스레드 안전 문제는 적절한 동기화 기법과 원칙을 준수함으로써 해결할 수 있으며, 실습을 통해 이를 익히는 것이 중요합니다. 스레드 안전한 코드를 작성함으로써 안정적이고 효율적인 멀티스레드 프로그램을 구현할 수 있습니다.