본문 바로가기
OS

4. Thread

by 긍정의고등어 2021. 5. 14.

1. Overview

🍠 Thread?

  • CPU 이용의 기본 단위

  • = Lightweight process

  • Thread의 구성 요소

    🛷 각 쓰레드별 요소

    • Thread ID
    • 프로그램 카운터(PC)
    • 레지스터 집합
    • 스택

    📝 같은 프로세스의 다른 쓰레드와 공유 요소

    • 코드
    • 데이터
    • 열린 파일 / 신호 등의 OS 자원

    🏐 모식도

🥎 Muti-thread programming 의 장점

  • 응답성(Responsiveness)
    • 다중 쓰레드화 하면 어느 한 요청의 응답시간이 길어지더라도 그 외 쓰레드로 다른 요청을 처리할 수 있다.
  • 자원 공유(Resource Sharing)
    • 기존의 프로세스간 통신(IPC)에서는 Shared Memory 혹은 Message Queue 방식을 사용해 자원을 공유했다.
    • 반면 쓰레드 끼리는 그들이 속한 프로세스 내에서 코드, 데이터 등을 별도의 작업 없이 공유할 수 있다.
  • 경제성(Economy)
    • 프로세스의 생성이나 프로세스간의 Context switching은 쓰레드의 그것에 비해 오버헤드가 크다.
  • 규모 적응성(Scalability)
    • Multi-core 환경에서 각각의 쓰레드가 각기 다른 core에서 Parallel하게 실행될 수 있다.

2. Multicore Programming

🏀 병행성(Concurrency)과 병렬성(Parallelism)

  • 병행성(Concurrency)

    • 여러 작업을 시분할하여 실행함으로써 동시에 실행되는 것 처럼 실행(논리적 개념)
    • 기존 Single-core 시스템에서의 병행성
      • T5가 추가된다면 다른 쓰레드 조각 사이 사이에 끼워져 실행된다.

  • 병렬성(Parallelism)

    • 여러 작업을 실제로 여러 작업 처리기(CPU)에서 동시에 병렬로 실행(물리적 개념)
    • Multi-core 시스템에서의 병렬성

  • 병렬성(Parallelism) 없이 병행성(Concurrency)를 가질 수 있다.

🎈 Multi-core 프로그래밍 고려사항

  • 태스크 인식(Identifying tasks)
    • 수행해야 할 전체 작업을 분석하여 독립된 병행 가능 task로 나눌 수 있어야 한다.
    • 이상적인 task는 서로 독립적이고 개별 코어에서 병렬 실행이 가능하다.
  • 균형(balance)
    • 각 task들이 균등한 기여도를 갖도록 나눌 수 있어야 한다.
  • 데이터 분리(Data splitting)
    • task를 나누는 것처럼 task가 접근하는 data 또한 개별 코어에서 가용하도록 분리할 수 있어야 한다.
  • 데이터 종속성(Data dependency)
    • 둘 이상의 task가 접근하는 data에 대해서는 task의 수행을 동기화 해야한다.
  • 테스트 및 디버깅(Test and debugging)

🔑 Amdahl's Law

  • Core가 많을수록 무조건 좋은가?에 대한 답을 얻을 수 있음
  • 기본 공식
    • S: 작업 중 반드시 순차적으로 실행되어야 하는 구성요소의 비율(병렬 X)
    • N: 코어의 개수

  • 상황별 예시

    • S = 0.25, N = 2 ⇒ speedUp = 1.6
      • 단순히 이론적으로만 생각하면 1개의 코어로 10초가 걸리면 2개의 코어가 있다면 속도는 2배가 되어야 한다.
      • 하지만 전체 작업 중 25% 정도만 병렬이 아닌 순차적으로 실행되어야 한다면 총 처리 속도는 2배가 아닌 1.6배가 더 빨라질 것이다.
    • S = 0.25, N = 4 ⇒ speedUp = 2.28
      • 위의 예시와 비교해서 코어의 수가 2배가 되어 단순 계산으로 속도는 3.2배가 되어야 한다.
      • 하지만 전체 작업 중 25%가 순차적 실행이므로 Amdahl의 계산에 의하면 2.28배 빨라질 뿐이다.
    • N과 S에 따른 속도 향상 그래프

  • 위 예시로 미루어 보아 순차 실행 요소가 포함되어 있다면 단순 core 수의 확장이 무조건적인 성능의 향상을 보장하지는 않는다.

🔦 병렬(Parallelism) 실행의 유형

  • Task 병렬 실행

    • task(쓰레드)를 다수의 core에 분배
    • task는 각자 고유한 연산을 수행

  • Data 병렬 실행

    • 동일한 Data의 부분 집합을 다수의 core에 분배
    • 각 core는 동일한 연산 수행


3. Multithreading Models

🔑 커널(kernel) 스레드와 사용자(user) 스레드

  • Kernel thread

    • OS에 의해 직접 지원, 관리됨
    • 현대 모든 운영체제에서 지원
  • User thread

    • 커널의 윗단에서 지원
    • 커널의 직접적 지원 X
  • Kernel, User thread

☝ 다대일 모델 (Many-to-One Model)

  • 다수의 user thread를 하나의 kernel thread에 할당

  • 쓰레드 관리는 user space의 쓰레드 라이브러리가 담당하므로 효율적

  • 단점: 하나의 user thread가 blocking system call을 하면 나머지 쓰레드들이 전부 기다려야함

  • 모식도

✌ 일대일 모델 (One-to-One Model)

  • user thread와 kernel thread를 하나씩 매핑

  • 하나의 user thread가 blocking system call을 해도 다른 쓰레드가 기다리지 않음

  • 단점: 하나의 user thread를 만들려면 kernel thread도 생성해야 하며 시스템에 오버헤드로 작용

  • 모식도

👌 다대다 모델 (Many-to-Many Model)

  • 여러 user thread와 kernel thread를 매핑

  • 필요한 만큼의 user thread를 생성이 가능하고 그에 상응하는 kernel thread가 병렬 실행될 수 있다.

  • blocking system call에 유연하다

  • 최근 시스템 코어 수가 증가함에 따라 일대일 모델을 많이 사용하고 일대일 모델과 다대다를 혼용한 Two-level Model도 많이 사용한다.

  • 다대다 모델 모식도


4. Thread Library

  • 개발자에게 쓰레드를 생성하고 관리하기 편한 API를 제공
  • 주로 사용되는 Thread Library
    • POSIX Pthreads
    • Windows thread library
    • Java thread libaray

🥎 Pthreads

  • POSIX(IEEE 1003.1c)에서 쓰레드 생성, 동기화를 위해 제정한 표준 API

  • 쓰레드의 동작에 대한 명세일 뿐, 구현은 아니다.

  • Pthread 예제

    #include <pthread.h>
    
    int sum;
    void *runner(void *param);
    
    int main(int argc, char *argv[]) {
        pthread_t tid;
    
        pthread_create(&tid, NULL, runner, argv[1]);
        pthread_join(tid, NULL);
    
        printf("sum = %d\n", sum);
    }
    
    void *runner(void *param) {
        int i, upper = atoi(param);
        sum = 0;
        for (i = 0; i <= upper; i++) {
            sum += i;
        }
        pthread_exit(0);
    }
    • pthread_create: Java의 new Thread()와 같이 새로운 쓰레드를 생성
    • pthread_join: 자식 쓰레드의 동작이 끝날때 까지 대기
    • void *runner: Java의 Runnable.run() 과 같이 쓰레드의 동작을 정의하는 함수
    • 쓰레드끼리는 전역 변수를 공유하므로 main 함수에서 sum을 프린트하면 쓰레드에서 sum을 수정한 값 그대로 출력된다.
    • ex) ./a.out 10 ⇒ "sum = 55" 출력

⚾ Java Thread

🏓 Thread를 생성하는 방법

  • 자바에서 쓰레드를 작성하는 방법은 Thread 클래스를 상속 받는 것, Runnable 인터페이스를 구현하는 것 2가지 방법이 있다.

  • 아래는 Thread, Runnable 두 가지 방법에 대해 동일한 예제를 적용하여 어떤 차이가 있는지 기록한다.

  • 공통예제: 1초~6초 사이의 딜레이를 주고 각 쓰레드의 시작, 끝 시간에 print문으로 동작을 확인

  • Thread

    • 내부에 여러 메소드가 존재하며 이 또한 Runnable 인터페이스의 구현체이다.

  • 여러 메소드 중 run() 메소드만 오버라이드하면 쓰레드를 작성할 수 있다.

  • 예제 코드

    Public class MyThread extends Thread {
      private static final Random random = new Random();
    
    @Override
    public void run() {
      String threadName = Thread.currentThread().getName();
      System.out.println("- " + threadName + " has been started");
      int delay = 1000 + random.nextInt(4000);
      try {
        Thread.sleep(delay);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("- " + threadName + " has been ended (" + delay + "ms)");
    }
    }
  • Runnable

    • Thread와 달리 메소드가 run() 하나밖에 없음
    • 인터페이스이므로 implements 하는 것 외에는 Thread와 동일
    • 예제 코드(run 함수 내부는 Thread와 동일)
    public class MyRunnable implements Runnable {
      private static final Random random = new Random();
    
      @Override
      public void run() {
        ...
      }
    }
  • 예제 실행

    • Thread의 실행은 해당 인스턴스의 start()를 호출(run이 아님)
    • Runnable은 Runnable형 인자를 받는 생성자를 통해 별도 Thread 객체 생성 후 start() 메소드 호출
    • 예제 코드
    public class ThreadRunner {
    
      public static void main(String[] args) {
        // Thread object 생성
        Thread thread1 = new MyThread("Thread #1");
        Thread thread2 = new MyThread("Thread #2");
    
        // Runnable object 생성
        Runnable runnable1 = new MyRunnable();
        Runnable runnable2 = new MyRunnable();
    
        Thread thread3 = new Thread(runnable1, "Thread #3");
        Thread thread4 = new Thread(runnable2, "Thread #4");
    
        // start all threads
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
      }
    }
    • 실행 결과

  • Thread와 Runnable의 차이

    • Thread 상속 시 다중 상속 불가능
    • Runnable은 인터페이스 이므로 다중 상속 가능, 실제로 Runnable을 많이 사용

🍠 Thread를 실행하는 또 다른 방법

  • 강의에서 언급했듯이 Java에서 쓰레드는 인스턴스 생성후 start() 메소드를 호출해서 실행할 수 있다.

  • Java 1.5 부터는 Executor 인터페이스를 제공하여 새로운 방식으로 쓰레드를 실행할 수 있게 되었다.

  • Executor.execute()로 쓰레드를 실행하는 방법

    Executor service = new Executor();
    service.execute(new Task());
    • execute의 인자는 Runnable을 넘겨주어야 한다.
    • Runnable은 리턴값을 갖지 않는다!
  • Executor.submit()으로 쓰레드를 실행하는 방법

    Class SumTo5 implements Callable<Integer> {
        public Integer call() {
            int sum = 0;
            for (int i = 1; i <= 5; i ++) {
                sum += i;
            }
            return new Integer(sum);
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            ExecutorService threadPool = Executors.newSingleThreadExecutor();
            Future<Integer> result = threadPool.submit(new SumTo5());
        }
    
        try {
            System.out.println("sum = " + result.get());
        } catch (InterruptedException | ExecutionException ie) {
            ...
        }
    }
    • submit의 인자로 Callable을 넘겨준다.
    • Callable은 쓰레드의 실행결과를 Future 객체로 반환한다.
    • 메인 쓰레드에서 Future 객체에서 get()을 하면 쓰레드의 결과값을 기다렸다가 가져와서 사용할 수 있다.

5. Implicit Threading(암묵적 쓰레딩)

  • 쓰레드의 생성과 관리 책임을 개발자 → 컴파일러나 런타임 라이브러리에 넘겨주는 것

🎈 Thread Pool

  • 기존 쓰레드 생성 방식의 문제점
    • 새로운 쓰레드가 필요할때 마다 new 키워드로 매번 새로운 쓰레드 인스턴스를 생성하는 것은 오버헤드가 크다.
    • 만약 쓰레드가 잠깐의 작업을하고 바로 폐기되어야 하고 한번에 여러 개의 쓰레드를 필요로 한다면 오버헤드는 더욱 커진다.
  • 프로세스를 시작할 때, 일정한 수의 쓰레드를 만들어 pool 안에 넣어 두었다가 필요할 때마다 꺼내서 쓰는 방식

🏐 Fork Join

🔦 OpenMP

'OS' 카테고리의 다른 글

5. CPU Scheduling  (0) 2021.05.31

댓글