개발자 끄적끄적
스레드와 멀티태스킹 본문
<멀티 태스킹(다중 작업)>
- 작업 or 태스크(task)
- 컴퓨터에서 처리하고자 하는 일의 단위
- 멀티 태스킹(multi-tasking)
- 한 시스템 내에서 여러 태스크를 동시 실행하는 기법
- 사용자 입장에서는 동시에 여러 작업을 처리 가능
- OS 입장에서는 여러 태스크를 관리해야하는 부담 -> 프로세스 단위의 실행의 문제점
- 전통적으로 태스크는 프로세스로 구성
- multi-processing
<배경>
- 한 응용 프로그램이 다수의 비슷한 작업을 수행할 필요가 있을 수 있다
- 예 : 웹 서버
- 단일 프로세스로 동작한다면, 한 번에 하나의 클라이언트만 서비스 가능
- 웹 서버에 요청이 들어오면 그 요청을 수행할 별도의 프로세스 생성산다면, 오버헤드
- 좀 더 가볍게 실행되며 효율적으로 처리될 수 있는 대안 필요
<프로세스 단위 멀티 태스킹의 문제점>
- 프로세스 '생성'의 큰 오버헤드
- 메모리 할당, 부모 복사, PCB 생성 등
- 프로세스 '컨텍스트 스위칭'의 큰 오버헤트
- 컨텍스트 교환에 시간적/공간적으로 큰 비용 발생
- 프로세스 간 '통신'의 어려움
- 기본적으로 각 프로세스는 독립된 공간을 가진다
- 프로세스 간 통신을 위해 별도의 방법 제공(복잡)
- signal, socket, message, queue, shared memory 등
<스레드란?>
- 스레드(thread)
- '프로세스'내에서 실행되는 흐름의 단위
- 일반적으로 한 프로세스는 다수의 스레드를 가짐
- 멀티 스레딩(multi-threading)
- 기존의 프로세스는 단일 스레드를 갖는 경우로 생각
- 멀티 스레드 운영체제(multi-threaded OS)
- 스레드를 실행 단위로 다루는 OS
- 현대의 거의 모든 OS의 처리 방식
*TCB(Thread Control Block)
- 스레드에 관한 다양한 정보를 저장하는 구조체
- 스레드 엔티티, 스케줄링 엔티티라고도 불린다
*커널은 CPU 스케줄링 시 전체 TCB 중 하나를 선택하여 스레드를 실행시킨다
<스레드의 특징(1)>
- 스레드는 실행단위이며 스케줄링 단위
- app 개발자에게는 작업을 만드는 단위
- OS에게는 실행 단위, 스케줄링 단위 : TCB(thread control block)
- 코드-데이터-힙-스택을 가진 실체
<스레드의 특징(2)>
- 프로세스는 스레드들의 컨테이너이다
- 스레드는 독립적으로 존재 불가
- 각 프로세스는 최소 1개 이상의 스레드를 가진다
- 스레드가 실행의 단위임
- 메인 스레드
- 프로세스 생성 시 커널에 의해 자동 생성되는 스레드
- 프로세스가 시작된다 == 메인 스레드가 시작된다
- 프로세스는 스레드들의 공유 공간을 제공
- 같은 프로세스 내의 스레드들은 '코드, 데이터, 힙' 공유
<스레드의 특징(3)>
- 스레드가 실행할 작업은 '함수'로 작성된다
- 스레드 구현 방법에 따라 OS나 스레드 라이브러리에게 이 함수를 스레드로 만들어 줄 것을 요청해야 한다
- 새 TCB 생성 후, 이 함수의 주소를 TCB에 등록
- 스레드의 생명주기
- 스레드로 지정된 함수 종료 시 해당 스레드 종료
- 스레드 종료 시 TCB 등 스레드 관련 정보는 모두 제거된다
- 한 프로세스에 속한 모든 스레도 종료 시 프로세스는 종료된다
<작업 스레드 맛보기 : pthread 라이브러리>
- 리눅스에서 POSIX 표준의 pthread 라이브러리를 이용하여 2개의 스레드로 구성된 멀티스레드 C응용 프로그램 만들기
#include <pthread.h> //pthread 라이브러리를 사용하기 위해 필요한 헤더
#include <stdio.h>
#include <stdlib.h>
void* calcThread(void* param); //스레드로 작동할 함수 선언
int sum = 0; //main 스레드와 calcaThread가 공유하는 전역 변수
int main(void){
pthread_t tid; //스레드의 id를 저장할 정수형 변수
pthread_attr_t attr; //스레드의 정보를 담을 구조체
pthread_attr_init(&attr); //attr을 기본값으로 초기화
pthread_create(&tid, &attr, calcThread, "100"); //calcThread 스레드 생성, 100은 ptheread_create가 호출될 때 전달할 인자값
/* 스레드가 생성된 후 커널에 의해 언젠가는 스케줄되어 실행 */
pthread_join(tid, NULL); //tid번 스레드의 종료를 기다린다
printf("calcThread 스레드가 종료하였습니다.\n");
printf("sum = %d\n", sum);
return 0;
}
void * calcThread(void* param); //calcThread가 호출되고, param에 "100"전달
{
printf("calcThread 스레드가 실행을 시작합니다.\n");
int to = atoi(param); //to = 100(문자열(Alphabet)->숫자(Integer)로)
int i;
for(i=1; i<=to; i++) //1에서 to까지 누적합 계산
sum+=i; //전역 변수 sum에 저장
--------------------------------------------------
결과
$gcc -o makethread makethread.c -lpthread
$./markthread
calcThread 스레드가 실행을 시작합니다.
calcThread 스레드가 종료하였습니다.
sum = 5050
$
- makethread의 실행과정
1. 프로그램 실행 시작, main 스레드 자동생성(TCB)
2. main 스레드가 calcThread 생성. 두 스레드가 번갈아 실행. 스레드의 실행순서는 알 수 없다(TCB(메인 스레드의 TCB) <-> TCB(calcThread 스레드의 TCB))
3. main 스레드는 calcThread 스레드의 종료를 기다리고 있고, calcThrerad는 for문을 실행하여 현재 1에서 50까지 합을 sum에 저장한 상태
4. calcThread() 함수가 종료하면 calcThread 스레드 종료. main 스레드는 sum 값 5050을 화면에 출력
<멀티 스레드 응용 프로그램의 예>
- 미디어 플레이어(동영상 재생기)
미디어 입력 스레드(네트워크나 하드디스크)
디코딩 스레드
1. 비디오 스레드
2. 오디오 스레드
- 테트리스 게임
오디오 스레드
블록을 아래로 떨어뜨리는 타이머 스레드
키를 입력 받아 블록의 모양과 방향을 바꾸는 키 처리 스레드
*각각의 작업들이 동시에 처리되어야 한다
<동시성과 병렬성>
- 동시성(concurrency)
- 1개의 CPU에서 2개 이상의 스레드가 (거의) 동시에 실행 중인 상태
- 병렬성(parallelism)
- 2개 이상의 CPU에서 각 스레드를 동시에 실행
<스레드 주소공간>
- 스레드가 사용하는 메모리 공간
- 스레드가 실행되는 동안 접근 가능한 메모리 영역
- 프로세스 주소 공간 내에 형성
- 스레드 주소 공간의 요소들
- 스레드 사적 공간
- 스레드 코드(Thread code)
- 스레드 로컬 스토리지(TLS, Thread local storage) -> 각 스레드만의 정적 데이터
- 스레드 사이의 공유 공간 -> 모든 공간에서 공유할 수 있는 공유 공간
- 프로세스의 코드
- 프로세스의 데이터 공간(로컬, 스토리지 제외)
- 프로세스의 힙 영역
#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(void){
char* p[2] = {"100", "200"};
int i;
pthread_t tid[2]; //스레드의 id를 저장할 정수 배열
pthread_attr_t attr[2]; //스레드 정보를 담을 구조체
for(i=0; i<2; i++){
pthread_attr_init(&attr[i]); //구조체 초기화
pthread_create(&tid[i], &attr[i], calcThread, p[i]); //스레드>생성
printf("calcThread 스레드가 생성되었습니다.\n");
}
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);
int i, sum=0;
for(i=1; i=atoi(param); i++) //1~param까지 더하기
sum += i; //지역변수에 저장
tsum = sum; //TLS 변수 tsum에 합 저장
printsum();
total += sum; //전역 변수 total에 합 누적
}
/* 모든 스레드가 호출할 수 있는 공유 함수 */
void printsum(void)
{
printf("계산 후 tsum = %d\n", tsum);
}
--------------------------------------------------
결과(스레드는 순차적으로 실행되는 것이 아닌 동시(병행적으로 실행된다)에 실행된다
$gcc -o mtTLS makethreadwithTLS.c -lpthread
$./mtTLS
calcThread 스레드가 생성되었습니다. //첫 번째 스레드 생성
스레드 생성 초기 tsum = 5
calcThread 스레드가 생성되었습니다. //두 번째 스레드 생성
계산 후 tsum = 5050 //첫번째 스레드의 고유한 값 생성
스레드 생성 초기 tsum = 5
계산 후 tsum = 20100 //두번째 스레드의 고유한 값 생성
calcThread 스레드가 종료하였습니다.
calcThread 스레드가 종료하였습니다.
total = 25150
$
- 프로세스/스레드 주소 공간의 변화 과정
1. 프로세스가 적재되고, main 스레드가 생성되어 main 스레드의 주소 공간 형성
2. 첫 번째 calcThread 스레드가 생성되어 프로세스 내에 주소 공간 형성
3. 2번째 calcThread 스레드가 생성되어 주소 공간을 형성하고, 첫 번째 calcThrad가 1에서 100까지 합을 구한 상태. 그리고 2번째 calcThread 스레드는 1부터 9까지밖에 합을 구하지 못한 상태
4. 첫 번째 calcThread 스레드가 종료하여 주소 공간이 소멸되고, 2번째 calcThread 스레드가 1에서 200까지의 합을 구한 상태
<확인 질문>
- 전체 몇 개의 스레드가 실행되는가? 3개(메인 스레드 + 2개의 작업 스레드)
- tsum은 어떤 변수인가? 그 특징은?
- 스레드 전역 변수
- 각 스레드 마다 독립적으로 사용되는 전역 변수
- tsum과 total의 차이점은?
- tsum : 각 스레드에 국한된 전역 변수
- total : 현재 스포레스의 전역 변수, 각 스레드에서 접근
<스레드의 기본 상태>
- OS마다 조금씩 상이
- 스레드 상태는 TCB에 저장된다
<스레드에 대한 연산/운용(operation)>
- 스레드 생성
- 프로세스 생성 시 main 스레드 자동 생성
- 한 스레드는 다른 작업 스레드 생성 가능
- 부모-자식 관게를 가지나 약한 관계를 지님
- 부모 스레드는 자식 스레드 id로 제어 가능
- 스레드 종료
- 프로세스 종료 시 그에 속한 모든 스레드 종료
- 임의의 스레드에서 'exit()' 호출 시
- 부모 스레드가 종료되더라도 자식 스레드가 종료되지는 않는다
- pthread_exit() 호출 시 해당 스레드만 종료된다
- 한 스레드가 다른 스레드를 강제 종료하는 방법은 일반적으로 제공하지 않는다
- 약속된 신호 전달하고, 신호를 수신한 스레드는 스스로 종료
- 공뷰 변수를 통한 확인으로 지연 종료
- 스레드 조인(join)
- 한 스레드가 다른 스레드의 '종료'를 대기
- 보통은 부모가 자식이 작업을 끝마치기를 기다릴 때 사용
- ex)
T1 스레드(부모 스레드) -> T1에서 pthread_create()(스레드 T2 생성) -> T2 스레드 시작(자식 스레드)
T1 스레드 -> T1에서 pthread_join()(스레드 조인, T2가 종료할 때까지 대기) -> T2종료
- 스레드 양보(yield)
- 실행 중인 스레드가 스스로 CPU를 다른 스레드에게 '양보하기'위해 실행을 중단하는 행위
- 양보한 스레드는 ready 상태가 된다
<스레드 컨텍스트(thread context)>
- 스레드가 실행 중인 상태 정보
- CPU 레지스터들 값
- TCB에 저장
<TCB의 구성 요소>
- 스레드 정보
- tid, state
- 컨텍스트
- PC, SP, 다른 레지스터들
- 스케줄링
- 우선순위, CPU 사용시간
- 관리를 위한 포인터들
- PCB주소, 다른 TCB에 대한 주소, 블록 리스트/준비 리스트 등
<스레드 제어 블록(TCB, thread control block)>
- 스레드를 실행 단위로 다루기 위해 스레드에 관한 정보를 담은 구조체
- 스레드 엔터티(thread entity), 스케줄링 엔터티(scheduling entity)라고도 불린다'
- 커널 영역에 생성되고, 커널에 의해 관리된다
- 스레드가 생성될 때 생성
- 스레드가 소멸되면 제거
<스레드 컨텍스트 스위칭>
- 현재 실행 중인 스레드를 중단시키고, CPU를 다른 스레드에게 할당하는 과정
- 현재 CPU에 존재하는 스레드 컨텍스트를 TCB에 저장하고, 새로 실행될 스레드 컨텍스트를 CPU에 적재
- 스레드 스위칭이라고 한다
- 커널이 CPU 자원을 한 스레드에서 다른 것으로 옮기는 작업
<컨텍스트 스위칭이 일어나는 경우>
- 시스템 호출 처리 과정 중에
- 스레드가 자발적으로 다른 스레드에게 양보 : sleep(), yield(), wait() 등에 의해
- 블록되는 경우 : read(), write() 등에 의해
- 인터럽트 발생으로 ISR 실행 중에
- 스레드의 시간 할당량이 만료된 경우
- 타이머 인터럽트에 의해
- I/O장치로부터 인터럽트가 발생한 경우
- 현재 스레드보다 우선순위가 더 높은 스레드의 I/O 작업이 완료된 경우
<컨텍스트 스위칭 과정>
1. CPU 레지스터 저장 및 복원
2. 커널 정보 수정
<컨텍스트 스위칭 오버헤드>
- 컨텍스트 스위칭은 기본적으로 CPU의 일
- CPU 시간 소모 -> 전환 시간이 길거나 잦은 경우 처리율 저하
-> 컨텍스트 스위칭 시간 최소화 필요
- 멀티 코어 CPU에서는 한 프로세스를 한 코어에 배치하거나
- CPU에 스레드 별로 레지스터 셋을 두기도
<컨텍스트 스위칭 오버헤드>
- 같은 프로세스로 :
- 같은 프로세스의 다른 스레도로 전환하는 경우
- 컨텍스트 저장/복원, TCB 리스트 조작, 캐시 플러시/채우기
- 다른 프로세스로 :
- 다른 프로세스의 스레도로 전환하는 경우
- CPU 주소 공간의 변경으로 추가적인 오버헤드 발생
*매핑 테이블의 내용 자체도 변경 -> 매핑 테이블을 교체 -> 추가 오버헤드 발생
<스레드의 구현 방법>
- 스레드의 스케줄링 주체에 따라
- 커널 레벨 스레드(kernel-level thread)
- 'OS의 커널' 의해 스레드를 운용
- 사용자 레벨 스레드(user-level thread)
- 커널의 도움 없이 사용자 공간에 구현된 '스레드 라이브러리'에 의해 스레드 운용
<커널 레벨 스레드 vs 사용자 레벨 스레드>
- 커널 레벨 스레드
- '시스템 호출'을 통해 스레드 생성
- 커널이 TCB를 커널 공간에 생성하고 소유한다
- 커널에 의해 스케줄된다
- 스레드 주소 공간(코드/데이터)는 사용자 공간에 존재
- main 스레드는 커널 스레드
- 사용자 레벨 스레드
- 스레드 '라이브러리 함수'를 호출해 스레드 생성
- 스레드 라이브러리가 TCB를 사용자 공간에 생성하고 소유
- 스레드 라이브러리에 의해 스케줄
<순수 커널 레벨 스레드>
- 부팅 때부터 커널의 기능을 돕기 위해 만들어진 스레드
- 커널 코드를 실행하는 커널 스레드
- 스레드 코드와 데이터는 모두 커널 공간에 형성
- 커널 모드에서만 작동
<멀티 스레드의 구현>
- app에서 작성한 스레드가 시스템에서 실행되도록 구현하는 방법
- 사용자가 만든 스레드가 시스템에서 스케줄되고 실행 되도록 구현하는 방법
- 스레드 라이브러리와 커널의 시스템 호출의 상호 협력 필요
- 종류(사용자 레벨 스레드 : 커널 스레드)
- N:1 매핑
- 1:1 매핑
- N:M 매핑
<N:1 매핑>
- OS가 모든 프로세스를 단일 스레드 프로세스로 다룸
- 각 프로세스마다 1개의 커널 레벨 스레드 생성
- 사용자 레벨 스레드는
- 스레드 라이브러리에 의해 스위칭 된다
- 커널 레벨 스레드가 스케줄 되야 실행가능
- 특징
- 단일 코어 CPU에서 멀티 스레드 응용 프로그램의 실행 속도가 전반적으로 빠르다
- 병렬성을 얻을 수 없다
- 사용자 레벨 스레드 블록시 프로세스가 블록된다
<1:1 매핑>
- 사용자 레벨 스레드마다 1개의 커널 레벨 스레드 생성
- 사용자 레벨 스레드는 커널 스레드 스케줄 시 실행
- 특징
- 개념이 단순해 구현이 용이
- 멀티 코어 CPU에서 높은 병렬성 제공
- 하나의 사용자 레벨 스레드가 블록 되어도 프로세스가 블록되지 않는다
- 커널에게는 부담
- 현대 많은 OS에서 채택
<N:M 매핑>
- N개의 사용자 레벨 스레드를 M개의 커널 레벨 스레드에 매핑
- 특징
- 1:1에 비해서는 커널의 부담이 적다
- 구현하기 복잡해 현대 OS에서는 거의 사용되지 않는다
<멀티 스레딩 app의 특징>
- 높은 CPU 이용률(활용률) : 멀티 코어 CPU 시스템 성능 향상
- 우수한 사용자 응답성 : 사용자에게 동시 접근성 향상
- 시스템 자원 사용의 효율성 : 프로세스에 비해 생성/유지 자원을 적게 사용
- 응용 프로그램의 구조 단순화 : app을 독립된 작업으로 분할 가능
- 쉽고 효율적인 통신 : 프로세스 간 통신에 비해 간단
<멀티 스레딩 한계>
- fork() 호출 시 : 자식 프로세스에는 fork()를 호출한 스레드만 존재
- exec() 호출 시 : 모든 스레드를 종료하고 새 실행 파일을 적재함
- 스레드 간 동기화 : 공유 데이터 접근 시 데이터 불일치 발생 가능
'운영체제' 카테고리의 다른 글
CPU 스케줄링 (0) | 2024.04.04 |
---|---|
프로세스와 프로세스 관리 (0) | 2024.03.22 |
운영체제와 인터럽트(Interrupt) (0) | 2024.03.13 |