Thead

dragonappear·2023년 6월 14일
0

Operating System 101

목록 보기
4/10


Thead

목적

  • 쓰레드는 CPU의 기본 실행 단위이다
  • 멀티 코어 시스템에서 처리율을 높이기 위해 멀티 쓰레드를 사용한다.

메모리 구조

  • 쓰레드는 쓰레드 ID, PC, 레지스터 집합, 스택으로 구성된다
  • 쓰레드는 같은 프로세스에 속한 다른 쓰레드와 코드, 데이터 섹션, 열린 파일, 신호와 같은 운영체제 자원들을 공유한다.

물리 메모리에서의 구조

  • 쓰레드는 프로세스의 물리 메모리 안에 위치한다.
  • 쓰레드의 스택 영역은 프로세스의 스택 영역을 프레임으로 나누어, 각 쓰레드의 스택 프레임이 위치하는 공간이고, 쓰레드별로 별도로 할당된다.
  • 쓰레드의 레지스터 영역은 레지스터 값이 저장되고 복원되어 쓰레드 간 문맥 교환 시 사용된다.
  • 프로세스의 힙 영역은 모든 프로세스 쓰레드가 동일한 힙을 사용한다

TCB

  • Thread Control Block
  • 운영체제가 쓰레드를 관리할 때, TCB 자료구조를 사용한다
  • TCB는 운영체제 내부에 생성되며, 쓰레드의 상태, 식별자, 레지스터 값, 스케줄링 정보, 스택 및 실행 컨텍스트 등 쓰레드와 관련된 정보를 포함한다.
  • 쓰레드 간의 동기화를 위한 동기화 객체(락,세마포어 등)에 대한 정보도 저장될 수 있다.

생성

쓰레드가 생성될 때 TCB가 생성된다.

제거

쓰레드의 실행이 완료되면 제거된다.

커널 쓰레드

  • 다수의 운영체제 커널은 다중화되어있다.
  • 커널 안에서 다수의 쓰레드가 동작하고 각 스레드는 장치 또는 인터럽트 처리 등의 특정 작업을 수행한다.
    • ex) Solaris는 커널 안에 인터럽트 처리를 위한 스레드 집합을 생성
    • ex) Linux는 시스템의 여유 메모리를 관리하기 위해 커널 스레드를 사용한다.

장점

  • 응답성

    • 사용자에 대한 응답성을 증가시킬 수 있다.
  • 자원 공유

    • 프로세스는 공유 메모리와 메시지 전달을 통해서만 자원을 공유한다. 프로그래머에 의해 명시적으로 처리되어야 한다.
    • 스레드는 자동적으로 자신이 속한 프로세스들의 자원들과 메모리를 공유한다.
  • 경제성

    • 생성

      • 프로세스 생성을 위해 메모리와 자원을 할당하는 것은 비용이 많이 드는 작업이다.
      • 스레드는 자신이 속한 프로세스의 자원들을 공유하기 때문에, 스레드를 생성하는 것이 프로세스를 생성하는 것보다 비용이 더 저렴하다.
    • 문맥교환

      • 프로세스 문맥 교환보다 스레드 문맥교환의 비용이 더 저렴하다.
  • 확장성

    • 다중 프로세서 구조에서는 각각의 쓰레드가 다른 처리기에서 병렬로 수행될 수 있기 때문에 확장성이 높다.

쓰레드 문맥 교환 VS 프로세스 문맥 교환

프로세스 문맥 교환은 쓰레드 문맥 교환 시간보다 더 오래 걸리며, 더 많은 오버헤드가 발생한다.

  • 문맥 교환 비용: 프로세스 문맥 교환은 프로세스의 상태를 저장하고 복원하는 작업으로 비용이 많이 든다. 반면에 쓰레드 문맥 교환은 프로세스의 주소 공간을 공유하기 때문에, 쓰레드 간의 문맥 교환이 더 빠르다. 쓰레드의 상태 저장 및 복원 작업은 프로세스 수준에서 이루어지는 것이 아니라, 스레드 라이브러리 수준에서 이루어지기 때문이다.

  • 스케줄링 오버헤드: 프로세스 문맥 교환은 다른 프로세스에 대한 스케줄링을 수행해야 하므로 추가적인 오버헤드가 발생한다. 반면 쓰레드는 동일한 프로세스 내에서 스케줄링되므로, 스케줄링 오버헤드가 줄어든다. 이로 인해 쓰레드 문맥 교환은 더 빠르게 이루어집니다.

쓰레드 문맥 교환

  • 쓰레드 문맥 교환은 동일한 프로세스 내에서 실행되는 다른 쓰레드 간의 전환을 의미한다.
  • 쓰레드는 주소 공간, 파일 디스크립터, 힙 메모리와 같은 자원을 공유하기 때문에 쓰레드 문맥 교환은 상대적으로 빠르게 수행된다.
    • 다른 쓰레드 간에 자원을 복사하거나 전달할 필요가 없기 때문이다.
  • 쓰레드 문맥 교환은 일반적으로 캐시 메모리 상태를 유지하고, 쓰레드의 레지스터 상태를 CPU 스택 메모리에 저장하여 수행된다

문맥 교환 시 CPU 스택 메모리를 사용하는 이유

  • 빠른 접근 속도

프로세스 문맥 교환

  • 프로세스 문맥 교환은 다른 프로세스 간의 실행 전환을 의미한다
  • 두 프로세스 간에는 독립적인 주소 공간, 파일 디스크립터, 힙 메모리 등을 가지고 있기 때문에 쓰레드보다 더 많은 작업이 필요하다.
  • 프로세스 문맥 교환은 메모리 매핑과 가상 메모리 상태를 변경해야 하며, 캐시 메모리를 지워야 한다.
  • 실행 중인 프로세스의 레지스터 값은 메모리에 저장되어야 한다.
  • 프로세스 문맥 교환은 일반적으로 캐시 메모리의 상태를 제거하고, 현재 프로세스의 레지스터 상태를 메모리에 저장하고, 다음 프로세스의 레지스터 상태를 복원하는 과정을 수행한다.

Multi-core Programming

  • 하나의 코어는 하나의 쓰레드만 실행할 수 있다.
  • 멀티 코어를 사용한다면 멀티 쓰레드를 더 효율적으로 실행시킬 수 있다.

암달의 법칙

암달의 법칙은 순차 실행(병렬 실행이 아닌) 구성 요소와 병렬 실행 구성 요소로 이루어진 시스템에서 코어를 추가했을때 얻을 수 있는 잠재적인 성능 이득을 나타내는 공식

전부 병렬 처리를 하는 것이 아니면 기대한 만큼의 속도증가는 나오지 않음을 볼 수 있다.

코어가 무조건 많은수록 더 빠르다고 할 수는 없다.


Multi-Threading Model

사용자 쓰레드, 커널 쓰레드

쓰레드는 크게 사용자 쓰레드, 커널 쓰레드로 나눌 수 있다.

  • 사용자 쓰레드는 커널 위에서 동작하고, 커널의 지원 없이 프로그래밍 언어나 라이브러리 수준에서 관리된다.
  • 커널 쓰레드는 운영체제에 의해 직접 지원되고 관리된다.

사용자-커널 쓰레드 간의 연관 관계가 존재해야 하는 이유

  • I/O, 시스템콜 처리
  • 병렬 처리
  • Blocking 작업 처리

사용자-커널 쓰레드 매핑 정보

  • 사용자-커널 쓰레드 매핑 정보는 운영체제 커널 내부에서 관리한다
  • 운영체제는 사용자-커널 쓰레드간의 매핑을 추적하고 유지하기 위해 테이블을 사용한다.
    • 필요에 따라 사용자 쓰레드 라이브러리나 런타임 시스템에서도 일부 관리될 수 있으나, 이러한 매핑 정보는 운영체제의 커널 내부의 테이블과 동기화 되어야 한다.

다대일 모델

  • 다수의 사용자 쓰레드가 하나의 커널 쓰레드로 매핑된다.

단점

  • 스레드 관리는 사용자 공간의 스레드 라이브러리에 의해 행해진다.
  • 한 쓰레드가 Blocking 시스템콜을 호출 할 경우, 전체 프로세스가 Blocking된다.
  • 한 번에 하나의 쓰레드만이 커널에 접근할 수 있기 때문에, 멀티 쓰레드가 멀티 코어 시스템에서 병렬로 실행될 수 없다.

멀티코어의 이점을 사용할 수 없기 때문에 이 모델을 사용 중인 시스템은 거의 존재하지 않음

일대일 모델

  • 각 사용자 스레드는 각각 하나의 커널 스레드로 매핑된다

장점

  • 하나의 스레드가 Blocking 시스템콜을 호출하더라도 다른 스레드가 실행될 수 있다.
  • 다중코어 시스템에서 스레드를 병렬로 수행하는 것을 허용한다.

단점

  • 사용자 스레드를 생성할 때 그에 따른 커널 쓰레드도 생성해야 한다.
  • 커널 쓰레드를 생성하는 오버헤드가 시스템 성능을 저하시킬 수 있다.
  • 대부분의 구현은 시스템에 의해 지원되는 스레드의 수를 제한한다.

Windows 계열의 운영체제들과 Linux 운영체제에선 일대일 모델을 구현하고 있다.

다대다 모델

  • 다수의 사용자 쓰레드가 그보다 작은 수 혹은 같은 수의 커널 쓰레드로 매핑된다.

장점

  • 다대일 모델에서 병렬처리가 안된다는 점과 1대1 모델에서 사용자 쓰레드가 너무 많은 커널 쓰레드를 생성할 수 있다는 것을 해결할 수 있다.
  • 개발자는 필요한만큼 많은 사용자 스레드를 생성할 수 있다.

Thread Library

사용되는 쓰레드 라이브러리는 POSIX Pthreads, Windows, Java를 예시로 들 수 있다.

Pthreads는 리눅스 커널 수준 쓰레드를 제공하고, Windows는 윈도우 커널 수준 쓰레드, JAVA는 JVM에서 동작하는 쓰레드(사용자 수준 쓰레드)를 제공한다.

Java의 경우 JVM이 쓰레드 관리를 책임진다.

자바의 경우 아래 세가지 방법을 사용하여 명시적으로 쓰레드를 생성할 수 있다.

  1. extends Thread
  2. implements Runnable
  3. Lambda

extends Thread

public class MyThead extends Thread {
    
    public static void main(String[] args) {
        MyThead myThead = new MyThead();
        myThead.start();
        System.out.println("Hello, my child!");
    }

    @Override
    public void run() {
        try {
            while (true) {
                System.out.println("Hello, Thread!");
                Thread.sleep(500);
            }
        } catch (InterruptedException e) {
            System.out.println("Interrupted!");
        }
    }
}

implements Runnable

class Sum {

    private int sum;

    public int getSum() {
        return sum;
    }

    public void plus(int plus) {
        this.sum += plus;
    }
}

class Summutation implements Runnable {

    private Sum sum;

    public Summutation(Sum sum) {
        this.sum = sum;
    }

    @Override
    public void run() {
        int sm = 0;
        for (int i = 0; i < 10; i++) {
            sm += i;
        }
        sum.plus(sm);

    }
}

public class MyRunnable {
    public static void main(String[] args) {
        Sum sum = new Sum();
        Thread thread1 = new Thread(new Summutation(sum));
        Thread thread2 = new Thread(new Summutation(sum));
        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
            System.out.println(sum.getSum());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

Lambda

public class MyLambda {
    public static void main(String[] args) {

        Runnable task = () -> {
            try {
                while (true) {
                    System.out.println("Hello, Lambda!");
                    Thread.sleep(500);
                }
            } catch (InterruptedException e) {
                System.out.println("Interrupted!");
            }
        };

        Thread thread = new Thread(task);
        thread.start();
        System.out.println("Hello, my child!");
    }
}

Implicit Threading

멀티 코어에서 멀티 쓰레딩을 프로그래머가 명시적으로 코드를 설계하는 것은 쉽지 않은 일이다.

이것을 해결할 수 있는 방법 중 하나는 스레딩의 생성과 관리 책임을 컴파일러와 런타임 라이브러리에게 넘기는 것이다.

스레딩의 생성과 관리 책임을 컴파일러와 런타임 라이브러리에 넘기는 것을 암시적인 쓰레딩이라고 한다.

암시적 쓰레딩으로 아래 3가지 방법을 사용할 수 있다.

Thread Pools

프로세스를 시작할 때 아예 일정한 수의 스레드들을 미리 풀에 만들어두는 방법

  • 스레드들은 해야할 작업이 없을때는 idle한 상태로 있는다
  • 그러다가 작업이 생기면 풀에서 한 스레드에게 작업을 할당하고
  • 작업이 종료되면 스레드는 다시 풀로 돌아가 다음 작업을 기다린다.
  • 이렇게 하다가 풀에 남아 있는 스레드가 바닥나면 가용 스레드가 생길때까지 기다려야 한다.

OpenMP

OpenMP는 C,C++ 로 작성된 API와 컴파일러 디렉티브의 집합

  • 개발자는 자신의 코드 중 병렬 영역(병렬로 실행될 수 있는 블록)에 컴파일러 디렉티브를 삽입하면, 이 디렉티브는 OpenMP 실행시간 런타임 라이브러리에 해당 영역을 병렬로 실행하라고 지시한다.
  • 시스템 코어 개수만큼 스레드를 생성한다.
  • 모든 스레드는 동시에 병렬 영역을 실행 하게 된다
  • 각 스레드가 병렬 영역을 빠져 나가면 스레드는 종료한다.

Grand Central Dispatch(GCD)

GCD는 애플 운영체제를 위한 기술이고, C 언어, API 및 런타임 라이브러리 각각을 확장하여 조합한 기술

  • C와 C++ 언어를 확장한 블록이라는 것을 식별한다 ex) ^{ printf("I'm block); }
  • 블록을 디스패치 큐에 넣어서 실행될 수 있도록 스케쥴링 한다
  • 큐에서 블록을 제거할 때 관리하고 있는 스레드 풀에서 가용 스레드를 선택하여 할당한다.
  • 직렬 큐에 넣어진 블록은 FIFO 순서대로 제거된다.
  • 블록은 큐에서 제거되면 다른 블록이 제거되기 전에 실행을 반드시 완료해야 한다.
  • 각 프로세스는 각자 직렬 큐(메인 큐)를 가진다.

쓰레드와 관련된 문제들

fork(), exec()

프로세스의 쓰레드가 fork()를 호출하면 새로운 프로세스는 모든 스레드를 복제해야 하는가 아니면 한 개의 스레드만 가지는 프로세스여야만 하는가?

몇몇 UNIX 계열 운영체제는 아래 2가지 fork()를 모두 제공한다.

  1. 모든 스레드를 복사한다.
  2. fork()를 호출한 스레드만 복제한다.

exec()을 호출하면 exec() 매개변수로 지정된 프로그램이 모든 스레드를 포함한 전체 프로세스를 대체한다.

어느쪽을 선택할 것인지는 응용 프로그램에 달려있다.

  • fork() 호출하자마자 exec()를 호출하면 모든 스레드를 다 복제해서 만들어 줄 필요가 없다.
  • exec()을 호출하지 않는다면 새로운 프로세스는 모든 스레드들을 복제해야 한다.

시그널 처리

시그널은 운영체제에서 프로세스에게 어떤 사건이 일어났음을 알려주기 위해 사용된다.

시그널은 동기식/비동기식으로 전달 될 수 있다. 동기식이건 비동기식이건 모든 시그널는 다음과 같은 형태로 전달된다

  1. 신호는 특정 사건이 일어나야 생성된다.
  2. 생성된 시그널가 프로세스에게 전달된다.
  3. 신호가 전달되면 반드시 처리되어야 한다.

동기식 시그널

  • 동기식 시그널로는 불법적인 메모리 접근, 0으로 나누기 등이 있다.
  • 동기식 시그널는 시그널를 발생시킨 연산을 수행한 동일한 프로세스에게 전달되기 때문에 동기식 시그널라고 불린다.

비동기식 시그널

  • 신호가 실행중인 프로세스 외부로부터 발생되면 그 프로세스는 신호를 비동기식으로 전달받는다.
    • ex) control + C 와 같이 특수한 키를 눌러서 강제 종료 시키거나, 타이머가 만료되는 경우 포함
  • 비동기식 시그널은 통상 다른 프로세스에게 전달된다
  • 모든 시그널은 둘 중 하나의 핸들러에 의해 처리된다
    • 디폴트 시그널 핸들러
    • 사용자 정의 시그널 핸들러

모든 시그널마다 커널이 실행시키는 디폴트 시그널 핸들러가 있다. 이 디폴트 시그널 핸들러는 신호를 처리하기 위하여 호출되는 사용자 정의 시그널 핸들러에 의해 대체될 수 있다.

단일 스레드 프로세스에서는 신호 처리는 간단하다. 신호는 항상 그 프로세스에게 전달되면 된다

하지만 멀티 스레드일 경우 어느 스레드에게 신호를 전달해야 하는가?

아래와 같은 선택이 존재한다.

  1. 신호가 적용될 스레드에게 전달한다
  2. 모든 스레드에게 전달한다
  3. 몇몇 스레드들에게만 선택적으로 전달한다
  4. 특정 스레드가 모든 신호를 전달받도록 지정한다

동기식 신호는 그 신호를 야기한 스레드에게만 전달되어야 하고, 다른 스레드에게 전달되면 안된다.

비동기식 신호는 명확하지 않다. 예를 들어 control + C 와 같은 경우 모든 스레드에게 전달되어야 한다.

운영체제에서는 스레드에게 받아들일 신호와 Block할 신호를 지정할 수 있는 선택권을 준다

스레드 취소

스레드 취소는 스레드가 끝나기 전에 그것을 강제 종료시키는 작업을 말한다.

  • 스레드 취소는 비동기식 취소, 지연 취소로 나눌 수 있다

      1. 비동기식 취소: 한 스레드가 즉시 목표 스레드를 강제 종료시킨다
      1. 지연 취소: 목표 스레드가 주기적으로 자신이 강제 종료되어야할지를 점검한다.
  • 스레드 취소가 어려운 점은 스레드들에게 할당된 자원을 회수할 때이다.

  • 스레드가 다른 스레드와 데이터를 공유하는 도중에 취소 요청이 오면 문제가 된다.

  • 운영체제는 취소된 스레드로부터 시스템 자원을 회수할 수도 있지만, 모든 시스템 자원을 다 회수하지 못하는 경우도 있다.

  • 따라서 비동기식으로 스레드를 취소하면 필요한 시스템 자원을 다 사용 가능한 상태로 만들지 못할 수도 있다.

  • 지연 취소는 스레드 자신이 취소되어도 안전하다고 판단되는 시점에서 취소 여부를 검사할 수 있다.

디폴트 취소 유형은 지연 취소이다.

스레드 로컬 저장소

  • 한 프로세스에 속한 스레드들은 그 프로세스의 데이터를 모두 공유한다.
  • 상황에 따라서는 각 스레드가 자기만 엑세스할 수 있는 데이터를 가져야 할 필요도 있다.
  • 그러한 데이터를 스레드 로컬 저장소라고 한다.

스케줄러 Activations

LWP

사용자 스레드 라이브러리와 커널 스레드 간의 통신 방법

운영 체제 사례

windows

  • 사용자 스레드와 커널 스레드는 1:1 관계

  • ETHREAD(executive thread block)

    • 실행 스레드 블록
    • 스레드가 속한 프로세스를 가리키는 포인터, 그 스레드가 실행을시작 해야할 루틴의 주소 등이 저장되어있고 KTHREAD에 대한 포인터도 가지고 있다.
  • KTHREAD(kernel thread block)

    • 커널 스레드 블록
    • 스레드의 스케줄링 및 동기화 정보를 가지고 있다
    • 커널 모드에서 실행될 때 커널 스택과 TEB에 대한 포인터도 가지고 있다.
  • TEB(thread environment block)

    • 스레드 환경 블록
    • 사용자 모드에서 실행 될때 접근되는 사용자 공간 자료구조
    • 스레드 식별자, 사용자 모드 스택, 스레드 로컬 저장소를 저장하기 위한 배열을 가지고 있다.

0개의 댓글

관련 채용 정보