[운영체제] 운영체제 2주차 스터디 (2) - 쓰레드

진승범·2024년 8월 22일
0

운영체제

목록 보기
3/4
post-custom-banner

Thread


쓰레드(Thread)란 프로세스 내에서 독립적으로 실행되는 단위로써, 실제로 작업을 수행하는 주체를 의미하고, 하나의 프로세스는 여러 개의 쓰레드를 가질 수 있다.

  • 병렬 처리
    쓰레드는 각각의 독립적인 실행 주체이기 때문에 병렬 처리가 가능하다. 이 때문에 한 개의 쓰레드를 사용하는 방법보다 더욱 더 빨리 작업을 처리 할 수 있다.
  • 자원 공유
    같은 프로세스 내의 쓰레드는 프로세스의 Stack을 제외한 메모리와 프로세스의 자원을 공유한다. 이는 쓰레드 간의 데이터 교환에 굉장히 효율적이고, 비용이 적다.
  • 경량
    쓰레드의 생성은 프로세스 생성에 비해 가볍고, 쓰레드 간의 전환도 더 빠르다.

웹 서버나, 데이터 베이스 등 실시간 애플리케이션에서는 쓰레드를 활용하여 높은 성능과 응답성을 제공한다. Spring Boot 에서 기본적으로 사용하는 Tomcat 서버도 기본적으로 여러 개의 쓰레드를 활용하여 클라이언트의 요청을 처리한다.

Thread의 메모리


좌측의 이미지는 일반적인 프로세스 메모리 공간의 모습이고, 우측의 이미지는 다수의 쓰레드가 생성된 프로세스 메모리 공간의 모습이다.
Thread 는 독립된 실행단위이기 때문에 각각의 Stack기타 레지스터 들을 각각의 Thread 마다 가지고 있다.

반면 Heap, Data(Static), Code 와 같은 영역은 그 어느 Thread 에서도 접근이 가능하도록 공유 되어있다.
이처럼 Thread는 프로세스 내의 Heap, Data(Static), Code 영역을 모두 공유하고 있어서, 언제 어디서든 각각의 영역에 접근이 가능하다.

그렇기 때문에 각각의 Thread에서 프로세스가 가지고 있는 Code 데이터를 이용해서 함수를 자유롭게 호출할 수 있다. Heap, Data 영역도 역시 공유되기 때문에 IPC 없이도
쓰레드 간의 통신이 가능하다. 이는 다수의 쓰레드가 통신으로 주고받고자 하는 데이터를 Heap 영역에 할당하여 각각의 쓰레드가 자유롭게 접근하여 사용한다고 생각하면 된다.

TCB (Thread Control Block)


쓰레드 제어 블록(Thread Control Block 이하 TCB) 이란 운영 체제에서 쓰레드를 관리하기 위해 사용하는 데이터 구조이다. TCB는 각 쓰레드의 상태와 제어 정보를 저장하여 쓰레드의 실행과 관리에 필요한 모든 정보를 포함한다.
PCB(Process Control Block)에 비해 적은 데이터를 저장하며, 이 때문에 Context SwitchingProcess 보다 더 빠르고 가볍다는 장점이 있다.

TCB에는 다음과 같은 정보가 포함 되어 있다.

  • 쓰레드 ID
  • 쓰레드 상태
  • CPU 정보
  • 우선 순위
  • PCB를 가리키는 포인터
  • 스레드 생성 스레드를 가리키는 포인터
  • Stack 포인터
    등이 있다.

Context Switching이 발생하면, 현재 실행 중인 위와 같은 스레드의 정보를 TCP에 저장하고, 다른 스레드의 TCB에 저장된 정보를 불러와 복원하여 실행한다.

Context Switching 시, TCB가 PCB보다 빠른 이유
PCB는 프로세스 전체의 상태를 관리하는 데이터가 포함되어 있으므로, 상대적으로 TCB에 저장되는 데이터보다 양이 많다. 스레드는 프로세스 안에서 작은 실행 단위이기 때문에 그렇다. 또한 스레드는 다른 스레드와 자원을 일부 공유하기 때문에 TCB에 관리할 데이터가 더 적다. 하지만 Process Context Swiching 시에는 사용하는 메모리 공간 자체가 아예 다르기 때문에 메모리 공간을 전환해야 하는 등 Thread Context Switching 보다 오버헤드가 더 발생한다고 이야기 할 수 있다.

User Level Thread 와 Kernal Level Thread

먼저 쓰레드는 2가지의 쓰레드로 크게 나뉜다.

  • User Level Thread
  • Kernal Level Thread

User Level Thread

먼저 User Level Thread는 커널에서 직접 생성하고 제어하는 것이 아니라, User Level에서 생성되고 관리된다. User Level Thread는 커널에서 해당 Thread를 인식하지 못하기 때문에 사용자가 Thread 관련
라이브러리를 구현하여 스케줄링하고 생성에 관여한다. 하지만 User Level Thread는 I/O 작업이 생길 경우, 다른 Thread 모두 대기 상태가 된다. 왜냐하면 커널은 User Level Thread에 대해서 인식하지 못하기 때문에
해당 I/O 작업이 일어난 Process대기(Blocked) 상태로 변경한다. 결국 해당 프로세스 내에 존재하는 모든 Thread가 대기 상태에 이르게 된다.

User Level Thread는 커널에 독립적으로 Thread를 스케줄링 할 수 있기 때문에 모든 운영체제에 적용할 수 있는 이식성이 높고, 스케줄링이나 동기화를 위해 커널 영역의 기능을 쓰지 않아, 커널 전환 시 발생하는 오버헤드가 줄어듭니다.
다만 커널이 User Level Thread들의 알지 못하면서 생기는 부수효과들이 문제가 될 수 있습니다. (e.g. I/O 작업 시 모든 Thread Blocked)

Kernal Level Thread

Kernal Level Thread는 커널이 Thread 생성과 스케줄링 등 관련된 모든 작업을 관리하는 방식이다. User Level ThreadKernal Level Thread가 One to One 매핑이 되고, 커널이 직접 스케줄링하고 관리하기 때문에 관리에 대해 많은 지원을 받을 수 있다.
하지만 오버헤드도 그만큼 늘어나게 된다. 또한 커널이 각 Thread를 개별적으로 관리하므로, 어떠한 Thread 에서 I/O 작업이 일어나도, 다른 쓰레드들이 대기 상태에 이르지 않고 작업을 계속 처리할 수 있다.

JVM의 Thread 구조는 어떻게 되어있을까?
https://letsmakemyselfprogrammer.tistory.com/98

Multi-Thread Programming

멀티 쓰레드 프로그래밍(Multi-Thread Programming) 은 하나의 프로그램이 여러 쓰레드를 사용해 동시에 여러 작업을 수행하는 프로그래밍 방식이다. 여러 작업을 동시에 처리할 수 있기 때문에 프로그램의 성능이 향상되지만 여러 쓰레드가 같은 메모리 자원에 접근하면서 생길 수 있는
동기화 문제로 인해 데이터가 깨질 수 있다. 이를 해결하기 위해 Lock 과 같은 동기화 메커니즘을 사용할 수 있다. 하지만 이 때문에 코드가 복잡해지고 데드락(DeadLock) 문제가 발생할 수 있다.

  • 장점

    • 멀티 쓰레드 환경에서 각각의 쓰레드는 메모리 공유가 가능하여 효율성이 증대한다. 이로 인해 데이터 복사 비용이 줄어들고, 통신 비용이 낮아진다.
    • 작업에 대해 병렬 처리가 가능하며, CPU를 조금 더 효율적으로 사용하여 애플리케이션의 응답성과 처리량과 성능을 향상 시킬 수 있다.
  • 단점

    • 단일 스레드 프로그래밍 방식보다 코드가 더 복잡해진다.
    • 스레드 간의 동기화 문제가 발생하여, 데이터의 정합성이 꺠질 수 있다. 이러한 경우 일관성을 유지하기 위한 Lock(락)과 같은 동기화 메커니즘을 사용하여 데이터를 일관성 있게 유지해야 한다.
    • 이 때문에 잘못 사용하게 되면 오히려 애플리케이션의 성능을 떨어리거나 안정성을 해칠 수 있다.

다음과 같은 코드를 통해 Java에서 멀티 쓰레드를 사용할 수 있다.

class MyThread extends Thread {
  public void run() {
      for (int i=0; i<5; i++) {
        System.out.println(Thread.currentThread().getName() + "-" + i);
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
      }
  }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        thread1.start();
        thread2.start();
    }
}

멀티 프로세스가 있는데, 멀티 쓰레드를 사용하는 이유

  • 멀티 프로세스는 각 프로세스가 독립적인 메모리 공간을 갖는데에 비해, 멀티 쓰레드 방식은 각 쓰레드가 StackRegister를 제외한 공간은 모두 공유한다. 그렇기 때문에 메모리 공간을 더 효율적으로 사용함으로써 메모리 효율성을 높일 수 있다.
  • 멀티 프로세스Context Switching 비용이 멀티 쓰레드 방식에 비해 높기 때문에 멀티 쓰레드 방식을 사용하게 되면 Context Switching 시 발생하는 오버헤드가 적고, Context Switching 속도가 빠르다.
  • 각 쓰레드 간에 데이터를 교환할 때에는 프로세스 처럼 IPC가 아니라 공유된 메모리를 통해 데이터를 공유할 수 있기 때문에 데이터 복사 및 통신 비용 또한 줄어들게 되고, 이로인한 코드 복잡성 역시 줄어들게 된다.
  • 멀티코어 환경에서 멀티 프로세스, 멀티 쓰레드 모두 병렬 작업 처리가 가능하지만, 앞서 이야기한 멀티 프로세스 방식에서는 오버헤드가 멀티 쓰레드 방식보다 더 발생하기 때문에 자원을 더 효율적으로 활용하는 데에 있어서는 멀티 쓰레딩 프로그래밍이 더 적합하다.

멀티 쓰레딩 프로그래밍 시 주의할 점

  • 멀티 쓰레딩 프로그래밍 문제의 핵심은 바로 하나의 자원에 대해 여러 쓰레드가 접근이 가능하다는 점이다. 만약 불변해야 하는 데이터가 여러 쓰레드 중 하나의 쓰레드에 의해 변경이 되었다면 그 값을 읽어서 사용하는 다른 쓰레드들은 모두 영향을 받거나, 최악의 경우 작업을 정상적으로 처리할 수 없게 된다. 이러한 동기화 문제를 해결하기 위해서 Lock(락)과 같은 동기화 메커니즘을 도입해서 해결할 수 있는데, 이 역시도 DeadLock과 같은 문제점을 야기할 수 있기 때문에 멀티 쓰레딩 프로그래밍 시 교착 상태에 빠지지 않게 하거나, 동기화 문제를 주의하며 애플리케이션을 개발해야 한다.

Thread-Safe 란?
Thread-Safe란 위에서 제시한 멀티 쓰레딩 프로그래밍 시 주의할 점의 문제를 해소한 상태이다. 즉, 멀티 쓰레드 프로그래밍에서 공유된 자원에 대해 동시에 접근이 이루어졌을 때, 프로그램 실행에는 아무 문제가 없는 상태를 말한다. 이 상태에서는 두 개 이상의 쓰레드가 Race Condition에 빠지지 않고, 결과에 대한 정합성이 보장 되었음을 나타낸다.
이에 대해 동기화에 대한 범위를 최소화하거나, 자원을 얻는 순서나 타임 아웃에 대해 설정하고, 가능하면 불변 객체를 사용하여 접근하는 자원에 대해 그 어떤 쓰레드에서도 자원을 변경할 수 없도록 설정한다면 Thread-Safe 하고 안전한 멀티 쓰레드 프로그래밍을 할 수 있다.

참고

https://mooneegee.blogspot.com/2015/01/os-thread.html

post-custom-banner

0개의 댓글