공부/Operating system

OS : Concurrency - Race Condition

상연 2022. 4. 28. 02:22

목차

    학부 수업 내용을 필기한 내용입니다.
    필자가 이해를 제대로 못하고 정리한 경우 틀린 내용이 있을 수 있습니다.
    그러한 부분이 있다면 댓글로 이야기하여 수정하게 해 주시면 감사하겠습니다.

    Why It Gets Worse : Shared Data

    공유된 자원이 있을 때 왜 악화될까?

    #include <stdio.h>
    #include <pthread.h>
    #include "common.h"
    #include "common_threads.h"
    
    static volatile int counter = 0;
    
    //mythread()
    //
    //Simply adds 1 to counter repeatedly, in a loop
    //No, this is not how you would add 10,000,000 to
    //a counter, but it shows the problem nicely.
    //
    void *mythread(void *arg) {
        printf("%s: begin\n", (char *) arg);
        int i;
        for (i = 0; i < 1e7; i++) {
            counter = counter + 1;
        }
        printf("%s: done\n", (char *) arg);    
        return NULL;
    }
    
    //main()
    //
    //Just launches two threads (pthread_create)
    //and then waits for them (pthread_join)
    int main(int argc, char *argv[]) {
        pthread_t p1, p2;
        printf("main: begin (counter = %d)\n", counter);
        Pthread_create(&p1, NULL, mythread, "A");    
        Pthread_create(&p2, NULL, mythread, "B");
        // join waits for the threads to finish   
        Pthread_join(p1, NULL);
        Pthread_join(p2, NULL);
        printf("main: done with both (counter = %d)\n", counter);
        return 0;
    }

    아까의 코드에서 mythread 부분이 좀 바뀌었다.

    보면 전역 변수로

    static volatile int counter = 0;

    이렇게 선언이 되어있고

    mythread()에서는...

    for (i = 0; i < 1e7; i++) {
            counter = counter + 1;
        }

    각 스레드에서 10,000,000 씩 증가시키게 되어있다.

    그리고 이러한 전역 변수 counter과 대비되는 것이 지역변수 i이다.

    그래서 구조를 보게 되면

    전역 변수는 Address space에서 Data 영역에서 공유가 되고 있으며

    각 스레드의 execute Stack에서 i는 별도로 갖게 되어있다.

    그럼 이를 실제로 실행하게 되면 어떻게 될 것인지 한 번 보자.

    위에 결과는 사람이 코드를 작성했을 때 기대한 결과이다.

    그러나 실제로는 2천만이 나오지도 않으며, 심지어 실행 때마다 결과마저 다르다.

    결과가 비결정적이다.

    왜 이런 일이 발생했을까?

     

    Counter 증가?

    여기에서 보면 문맥 교환이 2회 일어났다.

    mov 8049a1c, %eax

    이 명령어의 주소가 100 번지이기 때문에

    실행되기 이전에 PC는 100이었으며 eax는 0, counter 변수는 50인 상태였다.

    그리고 위의 명령어가 실행되면서

    PC는 그다음

    add $0x1, %eax

    주소인 105를 가리키며, eax는 8049a 1c주소(counter)에서 값을 받아 50이 되었다.

    그다음

    add $0x1, %eax

    이 되면서 eax 증가

    그다음에 스레드가 교체되면서 문맥 교환이 발생하는데...

    그다음에 T2도 T1과 같은 명령어를 수행한다 다만, 차이가 있다면 T2는

    mov %eax, 8049a1c

    까지 실행하면서 증가한 i값을 counter에 저장하는 것까지 완료하고

    그다음에 다시 T2에서 T1으로 문맥 교환이 발생한다.

    그러면 T1은 이전에 끊겼던 부분이 복원되면서

    108번지 명령어를 다시 실행하게 된다.

    mov %eax, 8049a1c

    그러면 여기쯤에서 생각을 해 보자.

    분명 처음 시작할 때 Counter의 값은 50이었다.

    그리고 현재까지 Add 명령어는 2번 실행되었다.

    Counter는 전역 변수이므로 2번 증가하게 되었다면 52가 되어야 하는데

    T1의 Store 연산이 끝난 후에 보이는 Counter의 값은 51이다.

     

    왜 이럴까?

    간단하다. T1이 증가한 값을 Counter에 저장하기 이전에 문맥 교환이 발생했고

    T2는 증가하지 않은 Counter를 받아 거기서 1을 증가시켰다.

    그리고 T2는 다시 문맥 교환을 하고 이전에 멈췄던 부분에서 증가시킨걸 그제야 반영했다.

    이 Counter를 증가하는 부분이 Atomic하지 않아 발생한 Concurrency 문제이다!

     

    The Wish for Atomicity

    그렇다면 이 증가하는 과정을 기계어 하나로 처리할 수 있으면 되지 않을까?(현재는 3줄)

    아니라면 이 세 개의 명령어를 Atomic 하게 만들 수 있다면?

    이것에 대해서는 천천히 알아보고 우선 용어에 대해 알아보자

    용어 설명

    • Critical Section
      • 일반적으로 변수 또는 데이터 구조인 공유 리소스에 액세스 하는 코드 조각입니다.
    • Race condition
      • 여러 실행 스레드가 거의 동시에 중요 섹션에 진입할 경우 발생합니다. 둘 다 공유 데이터 구조를 업데이트하려고 시도하여 놀라운(아마도 바람직하지 않은) 결과를 초래합니다.
    • indeterminate
      • 프로그램은 하나 이상의 'Race Condition'으로 구성됩니다. 프로그램의 출력은 실행마다 다릅니다. 어떤 스레드가 실행되었느냐에 따라 달라집니다. 따라서 결과는 우리가 보통 컴퓨터 시스템에서 기대하는 결정론적이지 않다.
    • mutual exclusion
      • 이러한 문제를 피하기 위해 스레드는 일종의 상호 배제 원형(Lock?) 사용해야 한다; 그렇게 함으로써 단 하나의 스레드만이 중요한 섹션에 진입하는 것을 보장하고, 따라서 경주를 피하고, 결과적으로 결정론적 프로그램 출력을 초래한다.

     

    현재 지금까지 우리가 본 상황에 대해 위의 용어를 통해 다시 나열하자면,

    Counter를 증가시키는 코드 조각들이 Critical Section이며, 따라서 Race Condition은 두 스레드가 같이 Counter를 증가하려고 시도하는 상황을 말한다. 이에 우리는 비결정적인 결괏값을 얻었는데 (2천만이 안나오고 실행마다 매 번 다른 결과값을 얻음) 이것이 indeterminate이다.

    이러한 문제를 피하기 위해 해야 하는 것이 mutual exclusion

    '공부 > Operating system' 카테고리의 다른 글

    OS : Lock - Controlling Interrupts  (0) 2022.05.03
    OS : Locks - The Basic Idea  (0) 2022.05.03
    OS : Thread - API  (0) 2022.04.28
    OS : Concurrency : 병행성 - Intro(2)  (0) 2022.04.28
    OS : Concurrency : 병행성 - Intro(1) + Thread  (0) 2022.04.28