본문 바로가기

C++

[C++]Linux - 스레드 동기화와 뮤텍스(Mutex)

반응형
반응형

멀티스레드를 이용하는 프로그램에서 여러개의 스레드가 공유 데이터에 접근하게 되면, 접근 시점에 따라 여러 문제가 발생할 수 있습니다.

이런 문제를 해결하기 위한 일련의 작업들을 스레드 동기화(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의 간단한 사용법이었습니다.

반응형