BASIC의 개발 노트
Concurrency 본문
Concurrency는 '동시성'이라는 뜻이다.
이 때 조심 해야 할 것은 '동시'라고 했지만 좀 더 엄밀하게 보면 동시에 실행되는 것'처럼' 보이는 것이다.
특히 싱글 코어에서는 하나의 코어 내에서 Thread 간 교차가 빠르게 일어나면서 명령을 처리하기 때문에
사용자 눈에는 마치 동시에 실행되는 것처럼 보여도 실제로 동시에 실행되는 것은 아니다.
만약 2개 이상의 코어를 갖고 있는 CPU가 있을 때, 각 코어 내 Thread가 실제로 동시에 실행될 수 있는데
이것을 우리는 Parallelism(병렬성) 이라고 하며 Concurrency 와는 미세한 차이가 있다.
물론 우리는 앞으로 계속해서 Concurrency를 다룰 것이기 때문에 싱글 코어 환경으로 생각하면 편하다.
Open File Descriptor : I/O 담당, I/O를 handling 할 때 File처럼 Read/Write를 한다.
PCB : Context Switching을 할 때 CPU의 Register 값을 담아놓는 영역
Thread를 만든다는 것은 별도의 실행흐름을 추가로 만드는 것이다. -> 그림에서 T1, T2
이 때 T1, T2를 위한 별도의 Stack이 반드시 필요하다.
Address Space만 놓고 볼 때 Process와 Multi Thread 환경의 차이는
Stack이 1개만 있냐 아니면 Thread 별로 1개씩 있냐의 차이라고 보면 된다.
모든 실행흐름은 1개의 CPU에서 서로 번갈아가면서 실행하는데
이 때 Thread 별로 Context를 저장하는 공간이 필요하고, 이 공간이 TCB(Thead Control Block)이다.
관련 내용을 좀 더 자세히 코드로 보면
main 함수가 호출되어서 생성된 main thread, main thread에서 만든 p1, p2 thread 가 있다.
위 코드에서는 p1, p2 각각의 Thread를 생성하고 join을 통해 p1, p2가 끝나길 기다린다.
Process와 달리 Thread는 시작할 때 무슨 일을 시킬지에 대한 내용을 함수로 정의하고 함수의 주소를 전달한다.
'Process와 달리'라고 했으니 Process에서 비슷한 일이 어떻게 동작하는지 보면,
multiprocess에서는 fork()를 사용해서 새로운 프로세스를 만들고, execution(execvp 함수)을 사용해서
무슨 일을 할지 정해주는데, execvp는 코드를 다시 loading하기 때문에 환경을 완전히 갈아엎는다.
즉 새로운 프로세스를 만들 때는 실행흐름을 먼저 만들고, 프로세스가 할 일을 새로 지정하는데
어떤 일을 해야하는지에 대해서는 실행 파일이 어디있는지 PATH를 알려주는 방식으로 지정한다.
정리하면 일을 지정하는 관점에서 볼 때, Thread의 경우 실행파일 바깥 쪽으로 나가지 않고 내부 코드에서
어느 부분을 실행하면 되는지 함수의 시작 주소를 지정해주는 방식이고,
Process는 코드 바깥 쪽, 즉 저장장치에 있는 실행 파일 PATH를 지정해주는 방식으로 서로 차이가 있다.
프로세스를 Control 할 때는 Process Control Block이 있는데,
여기에는 해당 프로세스의 상태(State, Running, Blocked, Ready 등), 프로세스의 속성(PID, kernel stack pointer 등), Context(CPU의 Register들) 등의 정보가 담긴다.
마찬가지로 Thread를 Control 할 때에도 Thread의 상태를 잘 보존하기 위한 Thread Control Block이 존재한다.
TCB의 구성요소는 간단하게 보면 State, Context, Thread id 등이 있다.
※ PCB와 TCB가 어떻게 존재하는지는 구현하기 나름이다.
PCB에서는 TCB가 어디 있는지는 알고 있다고 추정할 수 있다.
그러면 이제 위의 코드를 실행했을 때 Thread 생성이 어떤 과정을 거치는지 살펴보자.
우선 처음부터 20번 라인까지 main Thread 에서 쭉 실행이 되다가 join을 만났다.
join은 thread가 끝날 때 까지 기다리는 함수 이기 때문에 main의 State가 Running -> Blocked 상태로 들어간다.
이어서 Thread1가 Running 상태가 되고, Thread1에 주어진 일을 한다.
Thread1에서 할 일을 마치고 return을 하면 main의 State는 Blocked에서 Ready 상태로 들어가며
이어서 Ready에서 Running으로 바뀌고 21번 라인을 실행하는데 21번 라인은 T2를 기다리는 코드이다.
따라서 다시 Blocked 상태가 되고 Thread2가 Running 상태가 된다.
마찬가지로 Thread2에서 return을 하면 main의 상태는 Blocked->Ready->Running을 통해서 남은 라인을
마저 다 실행하고 main thread가 끝이 나게 된다.
하지만 반드시 이 Trace만 존재하는 것은 아니다. 스케쥴링이 어떻게 되느냐에 따라서 Trace는 다양하게 나타난다.
이 경우 Thread를 생성하자마자 Thread 쪽으로 실행흐름이 넘어간 케이스이다.
Thread1이 끝나고 return을 하자 다시 main으로 실행흐름이 넘어왔고 다시 Thread2를 생성했다.
마찬가지로 Thread2를 생성하자마자 실행흐름이 넘어갔고 Thread2를 끝내고 return 했다.
상태를 보면 Thread1, 2는 Ready->Running->끝, main은 Running->Ready->Running->Ready->Running->끝 이 된다.
다만 이 Trace에서 주의할 점은 main에서 join을 호출 했을 때 main의 상태가 Blocked로 가지 않는다는 것이다.
왜냐하면 이미 Thread의 일이 다 끝났기 때문에 join에서 바로 반환이 된다.
두 번째 join도 마찬가지이다. 결과를 바로 줄 수 있기 때문에 바로 반환이 된다.
즉 Blocked 되지 않고 계속 Running으로 진행되다가 끝이 난다.
이쯤되면 실행할 때 마다 흐름이 달라진다는 것을 눈치 챘을 것이다.
이 경우 T1과 T2를 다 만들었고. T1는 Ready 상태로 머물러 있으면서, T2가 실행이 됐다.
main Thread도 이 때는 Ready가 된다.
T2가 끝나면 main thread에서 T1에 대한 join을 호출했다. 이 때 상태가 Running -> Blocked 가 된다.
그리고나서 Thread1이 Running이 됐고 끝나서 return을 하게 되면 T2에 대한 join을 호출하지만
이미 앞에서 Thread2에 대한 일이 끝났기 때문에 바로 반환이 되서 Blocked 되지 않는다.
즉 Running 상태를 쭉 유지하다가 끝나게 된다.
이처럼 실행순서는 정해져있지 않고 예측할 수 없다.
'OS' 카테고리의 다른 글
Concurrency - Multi Processing vs Multi Threading (0) | 2021.06.05 |
---|---|
Concurrency - Race Condition (0) | 2021.06.04 |
File System - Read & Write + Directory (0) | 2021.05.26 |
File System (0) | 2021.05.25 |
RW Lock - Bad Case & Good Case (0) | 2021.05.24 |