Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
04-25 07:37
관리 메뉴

nomad-programmer

[Programming/C] Mutex (상호배제, 상호배타) 본문

Programming/C

[Programming/C] Mutex (상호배제, 상호배타)

scii 2020. 6. 21. 01:21

뮤텍스는 공유 데이터를 보호하는 락이다. 여러 스레드가 한 변수를 동시에 갱신하면, 결과는 예측할 수 없는 상태가 된다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>

// 2백만개
int number = 2000000;

void error(char *msg) {
    fprintf(stderr, "%s: %s", msg, strerror(errno));
    exit(1);
}

void *decay_number(void *t) {
    for (int i = 0; i < 100000; i++) {
        number = number - 1;
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    void *result;
    int ste;
    pthread_t thread[20];

    printf("스레드 연산 실행 전 number: %d\n", number);
    for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
        ste = pthread_create(&thread[i], NULL, decay_number, NULL);
        if(ste == -1){
            error("스레드 생성 실패");
        }
    }

    for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
        ste = pthread_join(thread[i], &result);
        if(ste == -1){
            error("스레드 종료 실패");
        }
    }
    printf("스레드 연산 실행 후 number: %d\n", number);

    return 0;
}

// 결과
/*

스레드 연산 실행 전 number: 2000000
스레드 연산 실행 후 number: 1827842

*/

결과값이 왜 예상치 못한 값이 되었을까? 모든 스레드가 다 실행된 후에 왜 number 변수는 0이 되지 않은 걸까?

:: 이유는 스레드가 서로 간섭을 일으켜 number 변수가 0이 되지 않았다. 그렇다면 왜 결과를 예측할 수 없을까? 그것은 스레드가 언제나 똑같은 시점에 실행되지 않기 때문이다. 어쩔 때는 서로 충돌을 일으키지 않고 어쩔 때는 충돌을 일으킨다.


스레드의 장점은 여러 수많은 일을 한꺼번에 처리하며 동일한 변수에 접근할 수 있다는 것이다. 
단점은 모든 스레드가 동일한 변수에 한꺼번에 접근한다는 것이다.

 

멀티스레드 프로그램은 강력하지만, 필요한 곳에서 적절히 제어하지 않으면 예측할 수 없는 작동을 할 수 있다. 
예를 들어, 자동차 두 대가 하나로 합쳐지는 길을 내려간다고 생각해보자. 사고를 막으려면 교통 신호등을 설치해야 한다.신호등이 있으면 차들이 동시에 공유된 자원(길)에 접근하지 않게 한다.

스레드 두 개 이상이 공유한 데이터 자원에 접근할 때도 마찬가지다. 두 스레드가 한 데이터에 동시에 접근하여 읽거나 쓰지 못하도록 신호등을 설치해야 한다.

두 스레드가 충돌하지 못하게 예방하는 신호등을 뮤텍스(Mutex)라고 부른다. 뮤텍스를 사용하면 아주 간단히 코드를 "스레드 안전" 상태로 만들 수 있다. (뮤텍스를 락(Lock)이라고도 부른다)

뮤텍스(MUTEX) = 상호(MUTually) + 배타(EXclusive)

 

뮤텍스를 신호등으로 사용하는 방법

스레드 간에 충돌할 수 있는 코드를 보호하려면 다음과 같이 뮤텍스를 만들어야 한다.

pthread_mutex_t a_lock = PTHREAD_MUTEX_INITIALIZER;

// PTHREAD_MUTEX_INITIALIZER 는 매크로이다. 전처리기가 이 매크로를 보면 뮤텍스를 만드는 코드로 치환해준다.

뮤텍스는 서로 충돌할 수 있는 모든 스레드가 볼 수 있어야 한다. 따라서 뮤텍스는 "전역 변수"로 만드는 게 일반적이다.

1. 빨간 불일 때 멈춘다.

중요한 코드가 시작되는 부분에 첫 번째 신호등을 설치해야 한다.
pthread_mutex_lock() 함수는 스레드 하나만 지나가게 만든다. 이 코드에 도착한 다른 스레드는 모두 기다려야 한다.

pthread_mutex_lock(&a_lock);
/* 중요 코드가 여기서 시작... */

2. 초록 불일 때 간다.

들어간 스레드가 중요 코드의 실행을 끝내면 pthread_mutex_unlock() 함수를 호출한다. 
이 함수를 호출하면 신호등을 다시 초록색으로 만들어 기다리던 스레드 중 하나가 중요 코드에 들어갈 수 있게 된다.

/* ... 중요 코드 끝 */
pthread_mutex_unlock(&a_lock);

 

MUTEX Lock & Unlock 예제

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>

// 2백만개
int number = 2000000;
// mutex lock
pthread_mutex_t decay_number_lock = PTHREAD_MUTEX_INITIALIZER;

void error(char *msg) {
    fprintf(stderr, "%s: %s", msg, strerror(errno));
    exit(1);
}

void *decay_number(void *t) {
    // mutex lock
    pthread_mutex_lock(&decay_number_lock);
    for (int i = 0; i < 100000; i++) {
        number = number - 1;
    }
    // mutex unlock
    pthread_mutex_unlock(&decay_number_lock);
    printf("number = %d\n", number);
    return NULL;
}

int main(int argc, char *argv[]) {
    void *result;
    int ste;
    pthread_t thread[20];

    printf("스레드 연산 실행 전 number: %d\n", number);
    for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
        ste = pthread_create(&thread[i], NULL, decay_number, NULL);
        if(ste == -1){
            error("스레드 생성 실패");
        }
    }

    for (int i = 0; i < (sizeof(thread) / sizeof(thread[0])); i++) {
        ste = pthread_join(thread[i], &result);
        if(ste == -1){
            error("스레드 종료 실패");
        }
    }
    printf("스레드 연산 실행 후 number: %d\n", number);

    return 0;
}

// 결과
/*

스레드 연산 실행 전 number: 2000000
number = 1900000
number = 1800000
number = 1700000
number = 1600000
number = 1500000
number = 1400000
number = 1300000
number = 1200000
number = 1100000
number = 1000000
number = 900000
number = 800000
number = 700000
number = 600000
number = 500000
number = 400000
number = 300000
number = 200000
number = 100000
number = 0
스레드 연산 실행 후 number: 0

*/

Q: 스레드를 사용하면 프로그램이 더 빨라지나?
A: 꼭 그런 건 아니다. 스레드를 사용하면 프로세스를 더욱 효율적으로 사용할 수 있지만, 락을 거는 방법에 주의해야 한다. 너무 자주 락을 걸면 단일 스레드 코드 만큼 느려질 수 있다.

Q: 어떻게 설계해야 스레드 코드가 빨라지나?
A: 스레드가 접근하는 데이터양을 가능한 한 줄이자. 스레드가 공유 데이터를 접근할 필요가 없으면 락을 걸 필요가 없고, 결국 코드가 훨씬 더 효율적으로 실행된다.

Q: 프로세스를 따로 만드는 것보다 스레드가 빠른가?
A: 일반적으로 스레드를 만드는 것보다 프로세스를 만드는 데 시간이 약간 더 걸리므로, 대체로 스레드가 더 빠르긴 하다.

 

데드락 (Dead Lock)

뮤텍스를 사용하면 '데드락'이 생길 수 있다.
가령 스레드가 두 개 있고 뮤텍스 A와 B가 있다고 치자. 이미 한 스레드가 A를 다른 스레드가 B를 갖고 있다.
B를 갖고 있는 스레드가 A를 가지려 하고,
A를 갖고 있는 스레드가 B를 가지려 하면 '데드락'이 발생한다.
두 스레드 다 원하는 뮤텍스를 가질 수 없기 때문에, 두 스레드 모두 더 이상 진행할 수 없는 것이다.

 

스레드 함수에 long형 값을 전달하는 방법

스레드 함수는 void 포인터형 인자 하나를 받고 void 포인터형 값을 반환할 수 있다. 
종종 스레드에 정수형 값을 전달하고 정수형 값을 반환받아야 할 때가 있다. 이때 long형 값을 사용할 수 있다. long형과 void 포인터형의 크기가 같기 때문이다.

// 스레드 함수는 void 포인터 인자를 한 개 받을 수 있다.
void* do_stuff(void* param) {
    // 원래대로 long형으로 변환
    long thread_no = (long)param;
    // 다시 void 포인터형으로 변환
    return (void*)(thread_no + 1);
}

int main(){
    pthread_t threads[3];
    long t;
    for(t = 0; t < 3; t++){
        // long형인 t 변수를 void 포인터 형으로 변환
        pthread_create(&threads[t], NULL, do_stuff, (void*)t);
    }
    void* result;
    for(t = 0; t < 3; t++){
        pthread_join(threads[t], &result);
        // 반환된 값을 사용하기 전 다시 long형으로 변환
        printf("스레드 %ld이(가) %ld를 반환하였다.\n", t, (long)result);
    }
    
    return 0;
}

// 결과
/*

스레드 0이(가) 1를 반환하였다.
스레드 1이(가) 2를 반환하였다.
스레드 2이(가) 3를 반환하였다.

*/

'Programming > C' 카테고리의 다른 글

[Programming/C] 자료형 범위의 값  (0) 2020.06.21
[Programming/C] static 키워드  (0) 2020.06.21
[Programming/C] 스레드 (Thread)  (0) 2020.06.20
[Programming/C] 소켓과 서버  (0) 2020.06.20
[Programming/C] 시그널 (Signal)  (0) 2020.06.20
Comments