멀티스레드를 이용하는 프로그램에서 여러개의 스레드가 공유 데이터에 접근하게 되면, 접근 시점에 따라 여러 문제가 발생할 수 있습니다.
이런 문제를 해결하기 위한 일련의 작업들을 스레드 동기화(Thread Synchronization)이라고 합니다.
스레드 동기화가 필요한 상황은 크게 2가지로 정리가 가능합니다.
1. 둘 이상의 스레드가 공유 자원에 접근할때
2. 스레드간 통신이 필요할 때(ex. 한 스레드가 작업을 완료한 후, 기다리는 다른 스레드에 알리는 경우)
아래는 리눅스에서 사용할 수 있는 여러 스레드 동기화 기법들입니다.
기법 | 설명 | 특징 | 주 사용 예 |
뮤텍스 (Mutex) | 단일 스레드만 임계 구역에 접근 보장 | - 코드 간단, 직관적 - 임계 구역 보호 - 데드락 주의 |
파일 읽기/쓰기, 공유 메모리 보호 |
조건 변수 | 특정 조건 충족 시 스레드 대기 | - 뮤텍스와 함께 사용 - 생산자-소비자 패턴에 적합 |
작업 큐 대기, 이벤트 발생 대기 |
세마포어 | 리소스 접근 스레드 수를 제어 | - 제한된 리소스 관리 - 이진 세마포어는 뮤텍스와 유사 |
데이터베이스 연결 관리, 소켓 풀 |
읽기-쓰기 잠금 | 읽기 작업은 동시에, 쓰기 작업은 단독 수행 | - 읽기 작업 많을 때 효율적 - 쓰기는 단일 스레드만 가능 |
공유 데이터 읽기/쓰기 |
스핀락 | 짧은 잠금 동안 활성 대기로 대기 | - 잠금 시간이 짧을 때 유리 - CPU 리소스 소모 가능 |
네트워크 패킷 처리, 짧은 임계 구역 보호 |
아토믹 연산 | 잠금 없이 동기화를 제공 | - 간단하고 빠름 - 카운터 증가/감소 작업에 적합 |
접속자 수 카운팅, 네트워크 트래픽 카운트 |
이벤트 기반 I/O | 이벤트 드리븐 방식으로 동기화 문제 최소화 | - 멀티스레드 대신 단일 스레드 사용 - 동기화 오버헤드 최소화 |
epoll, select, poll를 활용한 네트워크 처리 |
작업 큐 | 산자-소비자 패턴으로 작업 분산 처리 | - 작업은 큐에 추가, 워커 스레드가 처리 - 큐 접근 시 동기화 필요 |
네트워크 요청 분산 처리 |
이중 뮤텍스 방법을 구현해보도록 하겠습니다.
뮤텍스(Mutex)는 Mutual Exclusion, 상호 배제를 의미하며, 두 개 이상의 스레드가 공유 자원에 접근할때, 오직 한 스레드만 접근을 허용해야 하는 상황에서 사용합니다.
뮤텍스 사용의 구조는 아래와 같습니다.
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
// 임계 구역 코드
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
뮤텍스를 구현할때 사용하는 pthread_mutex_t는 POSIX threads(pthread)라이브러리에서 제공하는 데이터 타입입니다.
1) pthread_mutex_init()함수로 pthread_mutex_t를 초기화하고,
2) pthread_mutex_lock(), pthread_mutex_unlock()으로 뮤텍스를 잠금, 해제하며 임계 구역에서의 독립된 스레드의 작업을 제공합니다.
3) 이후 pthread_mutex_destroy()함수를 사용해서 mutex를 제거합니다.
실제 사용 예제 코드입니다.
스레드 함수 MyThread1은 공유 자원인 count변수의 값을 증가시키고, MyThread2는 count변수의 값을 감소시킵니다.
두 스레드 함수가 공유자원에 접근할때 mutex를 사용하여 한 스레드만 접근하도록 제어합니다.
#include <stdio.h>
#include <pthread.h>
#define MAXCNT 10000000
int count = 0;
pthread_mutex_t mutex; //initialize pthread_mutex_t
void *MyThread1(void *arg)
{
for (int i = 0; i < MAXCNT; i++)
{
pthread_mutex_lock(&mutex); //lock
count += 2;
pthread_mutex_unlock(&mutex); //unlock
}
return 0;
}
void *MyThread2(void *arg)
{
for (int i = 0; i < MAXCNT; i++)
{
pthread_mutex_lock(&mutex); //lock
count -= 2;
pthread_mutex_unlock(&mutex); //unlock
}
return 0;
}
int main(int argc, char *argv[])
{
pthread_mutex_init(&mutex, NULL); //initiatlize mutex
pthread_t tid[2];
pthread_create(&tid[0], NULL, MyThread1, NULL);
pthread_create(&tid[1], NULL, MyThread2, NULL);
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_mutex_destroy(&mutex); //destroy mutex
printf("count = %d\n", count);
return 0;
}
실행 결과는 아래와 같습니다.
count = 0
만약 여기서 mutex를 사용하지 않는다면 어떻게 될까요? 사실 단순히 생각하면 굳이 mutex를 사용하지 않아도 0이 출력되지 않을까 싶지만, mutex를 제거한 뒤 실행시켜보면 결과는 아래와 같습니다.
count = 922644
count = 807226
count = -2312168
.
.
.
이렇게 매 실행마다 다른 값이 출력됩니다. 스레드 동기화가 이루어지지 않았기 때문에, 두 스레드가 동시에 실행되면서 count에 대한 연산이 뒤섞일 수 있는 것입니다.
여기까지가 리눅스의 스레드 동기화 기법들과, 그중 한 기법인 mutex의 간단한 사용법이었습니다.
'C++' 카테고리의 다른 글
널(null) 포인터 nullptr에 대해 (0) | 2024.12.23 |
---|---|
const, constexpr, consteval에 대해 (0) | 2024.12.23 |
C++의 구조체 멤버 맞춤(Structure member alignment), 클래스 (0) | 2024.12.18 |