운영체제(Operating System) - 스레드와 멀티스레딩

프로세스의 문제점

프로세스 생성과 컨텍스트 스위칭의 큰 오버헤드
프로세스 사이의 통신 어려움

스레드 개념

스레드의 출현 목적

프로세스를 사용하는 문제점 해결을 위해
-> 프로세스보다 더 작은 스케줄링(실행)단위가 필요
-> 프로세스의 생성 및 소멸에 따른 오버헤드 저감
-> 빠른 컨텍스트 스위칭
-> 프로세스사이 통신에 대한 어려움 해소

스레드 개념

스레드는 실행단위이며 스케줄링 단위이다.

프로세스마다 PCB를 두고 프로세스 정보를 관리하듯이 스레드마다 TCB(Thread Control Block) 구조체를 두고 스레드 ID, 스케줄링 우선순위 등 스레드 정보를 관리하며 생성,소멸,스케줄링 등 스레드를 독립적인 단위로 다룬다 -> 운영체제는 TCB를 통해 스레드의 존재를 인식한다.

프로세스는 스레드들의 컨테이너이다.

프로세스를 생성할 때 커널은 자동으로 프로세스 내에 1개의 스레드를 생성하는데, 이 스레드를 메인 스레드라고 부른다. 프로세스가 실행을 시작한다는 것은 메인 스레드가 실행을 시작하는 것이다. 프로세스마다 PCB가 생성되고, 스레드마다 스레드 정보를 담는 TCB가 생성된다.

프로세스는 스레드의 공유 공간을 제공한다.

프로세스의 주소 공간 내에 스레드의 코드들이 적재되고
스레드들은 프로세스 내에 작성된 함수들을 호출할 수 있고 프로세스 내에 선언된 전역변수들을 액세스할 수 있으며 프로세스의 힙을 공유한다.
스레드가 동적 할당 받는 곳이 프로세스의 힙이고, 한 스레드가 동적 할당받은 메모리를 다른 스레드가 액세스할 수 있다.

스레드는 함수로 작성한다
- 응용 프로그램에서 스레드가 실행할 작업을 함수로 작성

  • 스레드를 만들어줄 것을 운영체제에게 요청해야 비로소 스레드 생성
  • 운영체제는 함수의 주소를 스레드 실행 시작 주소로 TCB에 등록
  • 운영체제는 TCB 리스트로 스레드 관리, 스레드 단위로 스케줄

스레드의 생명과 프로세스의 생명
- 스레드로 만든 함수가 종료하면 스레드 종료

  • 스레드가 종료하면 TCB 등 스레드 관련 정보 모두 제거
  • 프로세스에 속한 모든 스레드가 종료될 때 프로세스 종료

스레드 만들기

main()함수 - 스레드 코드. calThread 스레드 작성, calcThread 스레드의 종료를 기다린 후 합(sum 변수) 출력
calcThread() 함수 - 정수를 매개변수(param)로 받아 1에서 param까지 합을 구하여 전역 변수
sum에 저장
전역 변수 sum - calcThread와 main 스레드 모두 접근

#include <pthread.h> // pthread 라이브러리를 사용하기 위해 필요한 헤더 파일
#include <stdio.h>
#include <stdlib.h>
void* calcThread(void *param); // 스레드로 작동할 코드(함수)
int sum = 0; // main 스레드와 calcThread가 공유하는 전역 변수

int main() {
    pthread_t tid; // 스레드의 id를 저장할 정수형 변수
    pthread_attr_t attr; // 스레드 정보를 담을 구조체
    pthread_attr_init(&attr); // 디폴트 값으로 attr 초기화
    pthread_create(&tid, &attr, calcThread, "100"); // calcThread 스레드 생성
    // 스레드가 생성된 수 커널에 의해 언젠가 스케줄되어 실행
    pthread_join(tid, NULL); // tid 번호의 스레드 종료를 기다림
    printf("calcThread 스레드가 종료하였습니다.\n");
    printf("sum = %d\n", sum);
}
void* calcThread(void *param) { // param에 "100" 전달 받음
    printf("calcThread 스레드가 실행을 시작합니다.\n");
    int to = atoi(param); // to = 100
    int i;
    for(i=1; i<=to; i++) // 1에서 to까지 합 계산
        sum += i; // 전역 변수 sum에 저장
}

즉,

  • 프로세스가 생성되면 자동으로 main 스레드 생성: main() 함수 실행
  • 스레드 코드는 함수로 만들어짐: calcThread() 함수
  • 스레드 생성: pthread_create() 라이브러리 함수나 시스템 호출
  • 스레드마다 TCB 1개 생성
  • TCB에는 스레드의 시작 주소 저장. 이 주소에서 실행 시작
  • 스레드는 스케줄링되고 실행되는 실행 단위
  • 프로세스는 스레드들에게 공유 공간 제공

멀티스레드 응용 프로그램 사례



테트리스가 3개의 스레드로 구성된다는 것 = 3개의 TCB가 존재하고, 3개의 함수 주소가 각각 저장되어 있다.
일반적으로 함수는 다른 함수에 의해 호출되어 실행되지만 스레드 함수의 코드는 커널에 의해 직접 CPU가 실행하도록 제어된다.

멀티스레딩과 concurrency, parallelism

concurrency(동시성): 1개의 CPU에서 2개 이상의 스레드가 동시에 실행 중인 상태, 타임 슬라이스 단위로 CPU를 사용하도록 번갈아 스레드 실행

parallelism(병렬성): 2개 이상의 스레드가 다른 CPU에서 같은 시간에 동시 실행

스레드 주소 공간과 컨텍스트

스레드 주소 공간

스레드가 실행 중에 사용하는 메모리 공간
스레드의 코드, 데이터, 힙, 스택 영역

  • 스레드 사적 공간: 스레드 코드(Thread code), 스택, 스레드 로컬 스토리지(TLS, Thread local storage)
  • 스레드 공유 공간: 프로세스 코드, 데이터(로컬 스토리지 제외), 힙 영역

스레드 주소 공간에 대한 설명

스레드 코드 영역:

  • 프로세스의 코드 영역 사용
  • 스레드는 프로세스의 코드 영역에 있는 다른 모든 함수 호출 가능

스레드 데이터 영역:

  • 스레드가 사용할 수 있는 데이터 공간
  • 2개의 공간으로 구분
    1. 안전하게 지키고자 하는 데이터를 저장하기 위한 개별 스레드 전용 전역 변수 공간(스레드 로컬 스토리지)
    1. 프로세스에 선언된 모든 전역 변수들은 모든 스레드에 의해 공유-> 스레드 사이의 통신 공간으로 유용하게 사용

스레드 힙

  • 모든 스레드가 동적 할당받는 공간, 프로세스의 힙 공간 사용
  • 스레드에서 malloc()를 호출하면 프로세스의 힙 공간에서 할당받음
  • 동적 메모리의 주소만 알면 다른 스레드도 접근 가능(통신 공간)

스레드 스택

  • 스레드가 생성될 때마다 프로세스의 사용자 스택의 일부분을 할당받음
  • 스레드가 시스템 호출로 커널에 진입할 때, 커널 내에 프로세스에게 할당한 커널 스택에서 스레드를 위한 스택 할당

스레드 주소 공간 확인

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void printsum(); // 모든 스레드에 의해 호출되는 함수
void* calcThread(void *param); // 스레드 코드(함수)
static __thread int tsum = 5; // 스레드 로컬 스토리지(TLS)에 tsum 변수 선언
int total = 0; // 프로세스의 전역 변수, 모든 스레드에 의해 공유
int main() {
    char *p[2] = {"100", "200"};
    int i;
    pthread_t tid[2]; // 스레드의 id를 저장할 정수 배열
    pthread_attr_t attr[2]; // 스레드 정보를 담을 구조체
    // 2개의 스레드 생성
    for(i=0; i<2; i++) {
        pthread_attr_init(&attr[i]); // 구조체 초기화
        pthread_create(&tid[i], &attr[i], calcThread, p[i]); // 스레드 생성
        printf("calcThread 스레드가 생성되었습니다.\n");
    }
    // 2개 스레드의 종료를 기다린 후에 total 값 출력
    for(i=0; i<2; i++) {
        pthread_join(tid[i], NULL); // 스레드 tid[i]의 종료대기
        printf("calcThread 스레드가 종료하였습니다.\n");
    }
    printf("total = %d\n", total); // 2개 스레드의 합이 누적된 total 출력
    return 0;
}
void* calcThread(void *param) { // 스레드 코드
    printf("스레드 생성 초기 tsum = %d\n", tsum); // TLS 변수 tsum의 초기값 출력
    int i, sum = 0; // 지역 변수
    for(i=1; i<= atoi(param); i++) sum += i; // 1~param까지 더하기
    tsum = sum; // TLS 변수 tsum에 합 저장
    printsum();
    total+=sum; // 전역 변수 total에 합 누적
}
void printsum() { // 모든 스레드가 호출할 수 있는 공유 함수
    printf("계산 후 tsum = %d\n", tsum);
}


Q. 이 프로그램이 실행되는 동안 총 몇개의 스레드가 실행되는가?
A. main스레드 포함 총 3개가 실행된다.

Q. tsum을 어떤 변수라고 부르는가? 그리고 total 변수와의 차이점은 무엇인가?
A.Tsum은 각 스레드의 TLS 영역에 스레드마다 생기고 스레드에 의해 사적으로 사용. Total은 프로세스 전체에 하나만 생기고 프로세스에 속한
모든 스레드에 의해 공유

스레드 상태

  • 스레드는 생성, 실행, 중단, 실행, 소멸의 여러 상태를 거치면서 실행되며, 스레드 상태는 TCB에 저장됨.
  • 준비 상태(Ready) - 스레드가 스케줄 되기를 기다리는 상태
  • 실행 상태(Running) - 스레드가 CPU에 의해 실행 중인 상태
  • 대기 상태(Blocked) - 입출력을 요청, sleep()과 같은 시스템 호출로 중단된 상태
  • 종료 상태(Terminated) - 스레드가 종료한 상태

스레드 연산(operation)

스레드 생성

  • 프로세스가 생성되면 자동으로 main 스레드 생성. - 스레드를 생성 시스템 호출이나 라이브러리 함수를 호출하여 다른 스레드 생성

스레드 종료: 프로세스 종료와 스레드 종료의 구분 필요

  • 프로세스에 속한 아무 스레드가 exit() 시스템 호출을 부르면 프로세스 종료(모든 스레드
    종료)
  • main 스레드의 종료 – 모든 스레드도 함께 종료
  • 모든 스레드가 종료하면 프로세스 종료

스레드 종료

  • pthread_exit()와 같이 스레드만 종료하는 함수 호출 시 해당 스레드만 종료
  • main() 함수에서 pthread_exit()을 부르면 main 스레드만 종료

스레드 조인

  • 스레드가 다른 스레드가 종료할 때까지 대기

스레드 양보

  • 스레드가 자발적으로 yield()와 같은 함수 호출을 통해 스스로 실행을 중단하고 다른 스레드를 스케줄하도록 요청

스레드 컨텍스트

스레드의 실행중인 상태 정보
CPU 레지스터들의 값(PC, SP, 데이터/상태 레지스터 등)

  • PC 레지스터: 실행 중인 코드 주소
  • SP 레지스터: 실행 중인 함수의 스택 주소
  • 상태 레지스터: 현재 CPU의 상태 정보

스레드 제어 블록

스레드를 실행 단위로 다루기 위해 스레드에 관한 정보를 담은 구조체(스레드 엔티티(thread entity), 스케줄링 엔티티(scheduling entity))
커널 영역에 만들어지고, 커널에 의해 관리( Context switching 시 TCB 수정)

준비 리스트와 블록 리스트

준비 리스트

  • 준비 상태에 있는 스레드들의 TCB를 연결하여 관리하는 linked list
  • 스레드 스케줄링은 준비 리스트의 TCB들 중 하나 선택

블록 리스트

  • 블록 상태에 있는 스레드들의 TCB를 연결하여 관리하는 linked list
  • 대기하는 자원이나 I/O 장치별로 따로 구성

스레드 컨텍스트 스위칭

  • 현재 실행중인 스레드를 중단시키고, 다른 스레드에게 CPU 할당
  • 현재 CPU 컨텍스트를 TCB에 저장하고, 다른 TCB에 저장된 컨텍스트를 CPU에
    적재

스레드 스위칭이 발생하는 4가지 경우

  1. 스레드가 자발적으로 다른 스레드에게 양보
  • yield(), sleep(), wait() 등의 시스템 호출(혹은 라이브러리 호출)을 통해
  1. 스레드가 시스템 호출을 실행하여 블록되는 경우
  • read(), write() 등 I/O가 발생하거나 대기할 수 밖에 없는 경우
  1. 스레드의 타임 슬라이스(시간 할당량)를 소진한 경우
  • 타이머 인터럽트에 의해 체크
  1. I/O장치로부터 인터럽트가 발생한 경우
  • 현재 실행중인 스레드보다 더 높은 우선순위의 스레드가 블록상태로 I/O 완료
    를 기다리는 상황에서, I/O 완료되어 I/O장치로부터 인터럽트 발생
  • 인터럽트 받은 스레드 강제 중단(ready 상태로), I/O완료를 기다리던 높은 우
    선순위의 스레드로 스레드 스위칭

스레드 스위칭이 이루어지는 위치는 2가지

  1. 프로세스가 시스템 호출을 하여, 커널이 시스템 호출을 처리하는 과정
  2. 인터럽트가 발생하여 인터럽트 서비스 루틴이 실행되는 도중 커널 코드

스레드 스위칭 과정(스레드 A에서 스레드 B로)

  1. CPU 레지스터 저장 및 복귀
  • 현재 실행 중인 스레드 A의 컨텍스트를 TCB-A에 저장
  • TCB-B에 저장된 스레드 B의 컨텍스트를 CPU에 적재
  • CPU는 스레드 B가 이전에 중단된 위치에서 실행 재개 가능
  • SP 레지스터를 복귀함으로서 자신의 이전 스택을 되찾게 됨
  • 스택에는 이전 중단될 때 실행하던 함수의 매개변수나 지역변수들이 그
    대로 저장되어 있음
  1. 커널 정보 수정
  • TCB-A와 TCB-B에 스레드 상태 정보와 CPU 사용 시간 등 수정
  • TCB-A를 준비 리스트나 블록 리스트로 옮김
  • TCB-B를 준비 리스트에서 분리

컨텍스트 스위칭 오버헤드

구체적인 컨텍스트 스위칭 오버헤드

동일한 프로세스 내 다른 스레드로 스위칭되는 경우
1. 컨텍스트 저장 및 복귀

  • 현재 CPU의 컨텍스트(PC,PSP, 레지스터) TCB에 저장
  • TCB로부터 실행할 스레드의 스레드 컨텍스트를 CPU에 복귀
  1. TCB 리스트 조작
  2. 캐시 플러시와 채우기 시간

다른 프로세스의 스레드로 스위칭하는 경우

  • 다른 프로세스로 교체되면, CPU가 실행하는 주소 공간이 바뀌는 큰 변화로 인한 추가적
    인 오버헤드 발생
  1. 추가적인 메모리 오버헤드
  • 시스템 내에 현재 실행 중인 프로세스의 매핑 테이블을 새로운 프로세스의 매핑 테이블로 교체
  1. 추가적인 캐시 오버헤드
  • 프로세스가 바뀌기 때문에, 현재 CPU 캐시에 담긴 코드와 데이터를 무력화시킴
  • 새 프로세스의 스레드가 실행을 시작하면 CPU 캐시 미스 발생, 캐시가 채워지는데 상당한 시간 소

커널 레벨 스레드와 사용자 레벨 스레드

스레드 스케줄링 주체에 따라 2 종류의 스레드로 구분

  • 커널 레벨 스레드: 커널에 의해 스케줄링되는 스레드
  • 사용자 레벨 스레드: 스레드 라이브러리에 의해 스케줄링되는 스레드

커널 레벨 스레드

  • 응용프로그램이 시스템 호출을 통해 커널 레벨 스레드 생성
  • TCB는 커널 공간에 생성되며 커널에 의해 소유됨
  • 커널에 의해 스케줄
  • 스레드 주소 공간(스레드 코드와 데이터) : 사용자 공간 혹은 커널 공간에 존재
  • 순수 커널 레벨 스레드: 부팅할 때부터 커널 공간에서 실행되도록 작성
  • main 스레드는 커널 스레드
  • 응용프로그램을 적재하고 프로세스를 생성할 때 커널은 자동으로 main 스레드 생성

사용자 레벨 스레드

  • 응용프로그램이 라이브러리 함수를 호출하여 사용자 레벨 스레드 생성
  • 스레드 라이브러리가 스레드 정보(U-TCB)를 사용자 공간에 생성하고 소유
  • 스레드 라이브러리는 사용자 공간에 존재
  • 커널은 사용자 레벨 스레드의 존재에 대해 알 수 없음
  • 스레드 라이브러리의 스케줄러 코드에 의해 스케줄
  • 스레드 주소 공간(스레드 코드와 데이터) : 사용자 공간에 존재

커널 레벨 스레드와 사용자 레벨 스레드의 사례

프로세스 2개, 커널 레벨 스레드 4개, 사용자 레벨 스레드 3개일 때,

2개의 순수 커널 레벨 스레드: TCB1, TCB2

  • 스레드의 주소 공간은 커널에 있음. 부팅 때부터 커널에서 실행되도록 만들어진 스레드
  • 커널을 돕기 위해 만든 스레드(USB 인식, disk I/O flush, mouse handling, …)
  • 사용자와 대화하지 않으며 background에서 실행

2개의 커널 레벨 스레드

  • 프로세스당 하나의 커널 레벨 스레드(main 스레드) 자동 생성
  • TCB3
    - 커널은 단일 스레드 프로세스1을 적재할 때 자동으로 main 스레드 TCB3 생성
    - 커널이 프로세스1을 실행시키기 위함
  • TCB4
    - 커널은 멀티스레드 프로세스2를 적재할 때 자동으로 main 스레드 TCB4 생성
    - 커널이 프로세스2를 실행시키기 위함
  • TCB3과 TCB4의 스레드 주소 공간은 모두 사용자 공간에 있음

3개의 사용자 레벨 스레드

  • 프로세스2의 main() 함수가 라이브러리 함수를 호출하여 자신을 사용자 레벨 스레드로 등록
    - U-TCB1 생성
  • 프로세스2의 main() 함수가 라이브러리 함수를 호출하여 2개의 사용자 레벨 스레드 추가 생

    - U-TCB2, U-TCB3 생성
    스레드 스케줄링
  • 커널에 의한 스케줄
    - 코어1 : TCB2 실행(TCB2가 가리키는 커널 스레드 코드 2 실행)
    - 코어2 : TCB4 실행(TCB4가 가리키는 프로세스 내의 코드 실행)
    - 처음에는 프로세스2의 main()함수에서 실행을 시작하지만 현재 어떤 함수의 코드를
    실행하고 있는지 알 수 없음
    - 커널은 프로세스2 내에 하나의 스레드만 있다고 생각함

프로세스2에서의 사용자 스레드 스케줄링
- 스레드 라이브러리가 3개의 사용자 스레드 스케줄
예) main() 함수가 스레드 라이브러리의 yield() 함수를 호출하면 이 함수는 현재 대기중인 U-TCB2, U-TCB3 중에서 하나를 선택한다. 만일 U-TCB3이 선택되었다면, U-TCB1에 현재 실행 주소 등을 저장해 두고, U-TCB3에 저장된 실행시작 주소(스레드 코드 3)로 점프하여 실행 시작 -> U-TCB3이 스케줄 되었음

멀티 스레드 구현

응용프로그램에서 작성한 스레드가 시스템에서 실행되도록 구현하는 방법

  • 사용자가 만든 스레드가 시스템에서 스케줄되고 실행되도록 구현하는 방법
  • 스레드 라이브러리와 커널의 시스템 호출의 상호 협력 필요

N:1 매핑(N개의 사용자 레벨 스레드를 1개의 커널 레벨 스레드로 매핑)

3개의 사용자 레벨 스레드가 1개의 커널 레벨 스레드 TCB4 공유

장점: 단일 코어 CPU에서 멀티스레드 응용프로그램의 실행 속도가 전반적으로 빠름.
스레드 생성, 스케줄, 동기화 등에 있어 커널로 진입없이 사용자 공간에서 이루어짐

단점: 비효율적(프로세스에 속한 여러 사용자 레벨 스레드들의 병렬 처리 안됨), 하나의 사용자 레벨 스레드가 blocked되면 프로세스 전체 블록(프로세스 내 다른 사용자 레벨 스레드로 스위칭되지 못함)

1:1 매핑(1개의 사용자 레벨 스레드를 1개의 커널 레벨 스레드로 매핑, 사용자 레벨 스레드는 매핑된 커널 레벨 스레드가 스케줄되면 실행)

장점: 개념이 단순하여 구현이 용이함. 멀티 코어 CPU에서 멀티스레드 응용프로그램에게 높은 병렬성 제공. 하나의 사용자 레벨 스레드가 블록되어도 응용프로그램 전체가 블록되지 않음

단점: 사용자 레벨 스레드가 많아지면 모두 커널의 부담

N:M 매핑(N개의 사용자 레벨 스레드를 M개의 커널 레벨 스레드로 매핑. 2단계 스케줄링)

장점: 1:1 매핑에 비해 커널 엔티티 개수가 작아 커널의 부담이 적음

단점: 구현하기 복잡하여 현대의 운영체제에서는 거의 사용되지 않음

멀티스레딩으로 응용프로그램 작성할 때 장점

  • 병렬 실행(높은 실행 성능)
  • 우수한 응답성(한 스레드가 블록되어도 다른 스레드를 통해 사용자 인터페이스 가능)
  • 시스템 자원 사용의 효율성(스레드는 프로세스에 비해 생성, 유지 시 메모리나 자원 적게 사용)
  • 응용프로그램 구조의 단순화(작업 기준으로 응용프로그램을 여러 함수로 분할하고, 각 함수 별로 스레드를 만들어 동시 실행. 프로그램의 높은 확장성)
  • 작성이 쉽고 효율적인 통신

멀티스레딩에서 주의할 점

  • 여러 개의 스레드 중 한 스레드가 fork()를 호출한 경우-> 새로 생성된 프로세스는 fork()를 호출한 스레드로만 구성
  • 한 스레드가 exec()를 호출한 경우-> 현재 프로세스의 모든 스레드가 사라지는 문제
  • 스레드 사이의 동기화 문제
    -다수 스레드의 공유 데이터에 대한 액세스 시 공유 데이터 훼손 가능성->동기화 기법으로 해결

좋은 웹페이지 즐겨찾기