-
Thread 알아보기System 2022. 12. 13. 19:09728x90반응형
- 목차
같이 읽으면 좋은 글
https://westlife0615.tistory.com/285
소개.
Thread 는 프로그램의 실질적인 실행 단위입니다.
소위 프로그램이 실행되면, 실행된 프로그램을 Process 라고 하죠.Thread 는 이 실행 중인 Process 내부에서 다루어지는 가장 작은 실행 단위입니다.
실제로 CPU 를 차지해서 코드들을 순차적으로 실행하는 것이 Thread 입니다.
Thread 는 Task Scheduler (or Thread Scheduler) 에 의해서 Time-Shared 방식으로 CPU 를 점유합니다.
Task Scheduler 는 OS 의 구성요소로써 Thread 들의 Scheduling 을 관리합니다.
앞으로 알아보겠지만, Task Scheduler 이 Thread 의 우선순위와 상태를 고려해서 어떤 Thread 가 CPU를 차지할지 결정합니다.
그리고 Thread 가 실행되기에 앞서 Thread 는 Process 에 의해서 생성된 자식같은 존재이기 때문에
Process Scheduler 에 의해서 해당 Process 가 Running 상태가 되어야합니다.
그래서 순서적으로 Process 가 Process Scheduler 에 의해서 Running 상태가 되고,
Running Process 의 Thread 들이 Task Scheduler 에 의해서 Running 상태가 됩니다.
즉, multi-process & multi-threaded 환경에서 Process Scheduler 와 Thread Scheduler 가 유기적으로 동작해야합니다.
Thread 에 대해서 상세하게 알아보는 시간을 가지겠습니다.
Main Thread.
Main Thread 는 모든 프로세스가 하나 이상 가지는 기본적인 Thread 입니다.
보통 프로그램의 Entry Point 라고 부르는 main function 이 Main Thread 에 해당합니다.
main function 에 작성된 여러 코드 라인들. 즉 main function 에 작성된 실행 흐름들이 Main Thread 에 의해서 실행됩니다.
보통 HTTP 서버들의 (Spring 이나 gin-gonic 같은) Main Thread 를 예시로 들어보면,
Main Thread 에서 서버의 configuration 을 설정하고, tcp port 를 listening 하는 infinite loop 을 돕니다.
계속 HTTP request 를 체크하죠.
MySQL 같은 환경에서의 Main Thread 또한 이벤트 루프를 생성하여, MySQL 내부에서 발생하는 여러 이벤트를 수집하여 대처합니다.
이처럼 Main Thread 는 쉬게 멈추지 않게 설계하여 다른 Worker Thread IO Thread 들과 통신하게 됩니다.
프로그래밍 언어나 환경별로 다르겠지만,
Main Thread 가 종료되면 해당 Process 가 종료되는 케이스들이 많이 있습니다.
따라서 Main Thread 와 Worker Thread 의 설계 또한 중요해집니다.
What is Core.
Core 는 CPU 의 구성요소입니다.
Thread 나 Process 가 CPU 를 점유한다는 의미는
CPU 내부의 ALU (Arithmetic Logic Unit) 와 레지스터들을 점유하는 느낌인데요.
ALU 는 프로그램의 Instruction 들을 처리할 수 있는 Circuit 들을 가지고 있습니다.
연산을 위한 Adder 라든지, 부동소수를 처리하기 위한 Floating-Number Unit 과 연산의 결과를 저장하기 위한 레지스터들 있고,
Core 는 이러한 코드 실행을 위한 중요 요소들을 가집니다.
그래서 Core 의 수만큼 병렬처리가 가능해집니다.
Task Scheduler 에 의해서 Thread 의 Core 할당이 이뤄지는데요.
어떤 연산도 처리하고 있지 않은 Core 가 발견된다면 Task Scheduler 에 의해서 특정 Thread 들은 Core 에 할당됩니다.
Thread Control Block (TCB).
TCB 는 Thread 의 Execution Context (실행상태) 를 저장하는 자료구조입니다.
Context Switching 이라는 표현을 많이 쓰죠 ?여기서 말하는 Context 들이 TCB 에 저장됩니다.
즉, CPU 관점의 문맥 (Context) 를 뜻하는데요.
Time-Shared 방식으로 여러 Thread 들이 CPU 를 점유하다보니
Context Switching 에 의해서 CPU 를 재점유할 때, 과거의 실행 상태를 복원할 필요가 있습니다.
- 마지막으로 실행한 코드가 무엇인지 (Program Counter)
- 마지막으로 저장한 데이터들이 무엇인지 (Register, Stack Pointer)
등이 식별할 수 있는 데이터들을 TCB 에 저장하여 새롭게 CPU 를 점유할 때 매끄럽게 동작할 수 있습니다.
개별적인 요소에 대해서 알아보겠습니다.
Parent Process.
모든 Thread 는 태생적으로 하나의 Process 에서 파생됩니다.
Parent Process 는 자신을 만든 Process 의 id 가 저장됩니다.
반대로 Process 또한 자신이 생성한 Thread 의 정보를 가지고 있습니다.
Thread ID.
Thread 자신의 ID 입니다.
Program Counter.현재 실행 중인 코드의 Memory 상 주소입니다.
Program Counter 는 실행할 Code 의 메모리 주소를 가리키는데요.
Program Counter 가 저장되지 않으면 다음번 실행 시점에 매끄러운 실행 흐름을 유지하기 불가능합니다.
a++
a = a + 1 과 같은 Increment 로직을 실행 중이거나
while, for loop 를 실행 중인 상태에서 Program Counter 의 제대로 저장되거나 복원되지 않으면 프로그램의 결과는 기대한 결과와 달라집니다.
Registers.eax, ebx, ecx, edx 처럼 연산을 위해서 사용되는 레지스터들이 있습니다.
보통 mov eax, 1 처럼 데이터들을 레지스터에 임시로 저장하게 되는데요.
이 값들 또한 TCB 에 제대로 저장이 안되다면 문제가 되겠죠?
Stack Pointer.
Stack Pointer 는 Call Stack 이 push, pop 되면서 Call Stack 최상단 값을 저장하는 레지스터입니다.
Stack Pointer 은 Stack Frame 의 생성에 관여하는데요.
새로운 함수가 호출될 때마다 Stack Pointer 를 통해서 새로운 함수의 Base Pointer 를 생성합니다.
< Stack Pointer 에 대한 글을 같이 첨부합니다. >
https://westlife0615.tistory.com/285
Thread State.Thread 의 상태는 여러가지가 있습니다.
각 상태에 대한 설명과 예시를 알아보도록 하겠습니다.
1. New.
new 는 새롭게 생성된 Thread 입니다.
아래 코드처럼 생성을 마친 상태입니다.
Thread newThread = new Thread("newly-created-thread");
2. Runnable, Ready.
Runnable 은 시작된 Thread 입니다.
다만 Scheduler 에 의해서 실질적인 시작 상태는 아닙니다.
언제든지 Running 상태가 될 수 있는 상태입니다.
java 에서 start 를 끝마친 Thread 가 Runnable 상태가 됩니다.
Thread runableThread = new Thread("runnable-thread") { @Override public void run() { } }; runableThread.start();
3. Running.
Running 상태의 Thread 는 CPU 에 의해서 실행되고 있는 상태입니다.
아래 이미지는 visualVM 으로 Java 프로그램을 profiling 하는 이미지입니다.
아래 이미지를 보면 main 이라는 Thread 가 초록색, 보라색 progress bar 를 가집니다.
프로세스 실행 후 10초 동안만 Running 상태를 유지하고 나머지는 Sleep 으로 변경됩니다.
10초로 설정했지만, visualVM 에는 Running time 이 8초로 나오는군요.
<Running 상태에서 10초 이후에 Sleep 으로 변경>
public static void main(String[] args) throws InterruptedException { Instant tenSecondsAfter = Instant.now().plus(10, ChronoUnit.SECONDS); while (true) { if (Instant.now().isAfter(tenSecondsAfter)) { Thread.currentThread().sleep(10000000L); } } }
4. Blocked/Waiting.
Blocked/Waiting Thread 는 다른 Thread 와의 상호작용을 위해서 Blocked 된 상태입니다.
예를 들어,
어떤 전역 변수를 두 Thread 가 사용할 때 Synchronization 을 위해서 모든 Thread 들은 특정 전역 변수를 선점한 Thread 를 Waiting 해야합니다.
그 이후에 선점한 자원을 release 한 후에 다른 Thread 들이 해당 자원을 사용할 수 있습니다.
또 다른 예로 network IO 작업의 경우에 network IO 를 수행하는 IO Thread 의 작업이 종료되어야
네트워크 응답을 사용할 수 있게 됩니다.
여러가지 이유로 Thread 는 Blocked / Waiting 상태가 될 수 있습니다.
아래 이미지들은 Blocked 상태의 Thread 입니다.
3개의 Thread 가 존재합니다.
- 01-wait-after-10-seconds-thread
- 02-wait-after-10-seconds-run-after-20-seconds-thread
- main
01-wait-after-10-seconds-thread 은 10초 동안 Running 상태에서 10 ~ 20초 동안 Blocked 상태로 변경되고,
그 이후에 Running 상태로 변경되는 Thread 입니다.
02-wait-after-10-seconds-run-after-20-seconds-thread 는
01-wait-after-10-seconds-thread 의 Blocked 상태를 풀어주는 Thread 입니다.
그리고 종료됩니다.
<관련 로직>
아래의 예제는 sharedObject 를 공유하는 두 Thread 간의 Synchronization 과 그에 따른 Blocked 상태를 구현하였습니다.
public static void main(String[] args) throws InterruptedException { Object sharedObject = new Object(); Thread newThread2 = new Thread("01-wait-after-10-seconds-thread") { Instant tenSecondsAfter = Instant.now().plus(10, ChronoUnit.SECONDS); @Override public void run() { while (Instant.now().isBefore(tenSecondsAfter)) { } synchronized (sharedObject) { try { sharedObject.wait(); // The thread waits until it's notified. } catch (InterruptedException e) { // Handle interruption if needed. } } while (true) { } } }; newThread2.start(); Thread newThread3 = new Thread("02-wait-after-10-seconds-run-after-20-seconds-thread") { Instant twentySecondsAfter = Instant.now().plus(20, ChronoUnit.SECONDS); @Override public void run() { while (Instant.now().isBefore(twentySecondsAfter)) { } synchronized (sharedObject) { sharedObject.notify(); // The thread waits until it's notified. } } }; newThread3.start(); Instant tenSecondsAfter = Instant.now().plus(10, ChronoUnit.SECONDS); while (true) { if (Instant.now().isAfter(tenSecondsAfter)) { Thread.currentThread().sleep(10000000L); } } }
5. Terminated.
Thread 가 종료된 상태입니다.
종료된 Thread 와 관련된 리소스들은 제거 대상이 되며, TCB, PCB 에 기록된 데이터들이 제거됩니다.
다만 즉시 제거되진 않고 실행 환경에 따른 Clean up 스케줄을 따릅니다.
Call Stack In Thread.
Thread 별로 Call Stack 이 생성됩니다.
Heap 메모리의 경우에는 모든 Thread 들이 Process 의 Heap 메모리를 공유합니다.
하지만 Stack 메모리의 경우에는 Thread 별로 생성됩니다.
Thread 는 자신이 사용중인 레지스터를 기록하는데, 이때 base pointer 를 저장하는 ebp 레지스터 또한 저장됩니다.
ebp 의 값에 따라 Thread 자신의 Call Stack 을 사용할 수 있습니다.
추후에 작성할 내용들
Priority. Thread Queue. Thread Scheduling. Task Scheduler Thread Safety. Thread-Specific Registers.
반응형'System' 카테고리의 다른 글
IPC Signal 알아보기 (0) 2023.10.07 Shared memory communication 알아보기 (0) 2023.10.07 [memory management] page 알아보기 (0) 2023.09.22 Call Stack 이해하기 (0) 2023.09.21 리눅스 프로세스 (0) 2023.01.24