OS - (3) Threads

정선용·2022년 4월 2일
0

Operating System

목록 보기
3/12

threads

스레드란?

Thread란 프로세스의 작업(코드) 흐름 단위이며 CPU 이용의 기본 단위.
프로세스 내 주소 공간이나 자원 공유가 가능하다.

프로그램은 파일로 저장되어있으며 메모리에 올라가있지 않은 정적인 상태이고, 프로세스는 해당 파일(코드)들을 메모리에 올림으로써 동적으로 자원을 할당받거나 사용할 수 있는 상태가 된 것이다.
스레드는 그러한 프로세스가 작업을 실행할 때의 task들을 수행하는 더 작은 실행 단위 개념이다.

스레드는 처음부터 있었던 개념은 아니고 프로그램이 점점 복잡해지면서 프로세스 하나로 프로그램을 실행하기 어렵게되어 고안되었다
프로세스는 각각 할당된 메모리 내 정보에만 접근할 수 있는 제약이 있고 벗어나는 정보에 접근하면 오류가 발생하기 때문에 한 프로그램을 처리하기 위한 프로세스를 여러 개를 만들기도 어려웠다.
이러한 상황에서 고안된 것이 프로세스 안에 분리될 수 있는 여러 실행흐름을 수행하는 단위를 만드는 것이었고 그것이 스레드이며, 프로세스 특성 한계를 해결하기위해 만들어진 것이므로 프로세스와 다르게 스레드간 메모리를 공유하며 작동한다는 특징이 있다.

thread는 CPU입장에서는 cpu를 활용하는 최소 작업 단위이다.
cpu는 작업을 처리할 때 스레드를 최소 단위로 삼고 작업한다. 반면 OS는 그렇게 까지 자세한 단위까지 직접 작업하지 않기 때문에 운영체제 관점에서는 프로세스가 최소 작업 단위이다. OS는 process단위로 관리,처리해주고 실질적인 작업은 process가 어떻게 하든 상관이 없는것이다.
그래서 프로세스는 OS에게 하나의 최소단위이므로 그 안에 들어있는 스레드들은 같은 프로세스 소속으로 메모리를 자연스럽게 공유.


process는 pcb와 pid를 가진다. 그리고 stack으로 구현된다. (context와 여러개의 thread(stack))
thread도 마찬가지로 tcb와 tid를 가진다. 그리고 stack으로 구현된다.

프로세스 vs 스레드

  • 자원할당 unit vs cpu 이용 unit
    프로세스는 운영체제로부터 자원을 할당받은 작업의 단위 이며,
    스레드는 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위.
  • 메모리 영역 공유
    thread는 고유의 threadID,programcounter,Register집합,stack으로 구성되어있으며, 같은 프로세스에 속한 thread들과 Code, DateSection, 힙영역을 공유한다.

    프로세스는 고유한 code,data,heap을 가진다.
  • multiprocess vs multithread : 속도,switching,통신,생성
    프로세스의 경우, 두 프로세스가 하나의 데이터를 공유하려면 message passing이나 shared memory, pipe를 사용해야한다. 이는 효율도 떨어지며 관리 구현이 어렵다. 또한 context switching으로 성능 저하가 비교적 크다. 또한 fork 연산은 비용이 많이 드는 연산이다.
    하지만 thread는 context switching 비용이 비교적 작고, code, heap, data를 공유하므로 데이터 공유(통신)가 쉽고 빠르다. 또한 fork에 비해 스레드는 생성이 빠르다. 추가적으로 스레드는 병렬 실행이 허용되기도 한다. 다른 스레드의 실행 시간이 길거나 입출력 요청으로 block되더라도 계속 실행되는 것이 허용된다. 응답성이 좋다.=> 경량 process라고도 불린다.
  • 오류 발생 시.
    프로세스 실행 중 오류가 발생한다면, 공유하고 있는 파일을 손상시키는 것이 아니라면 아무 영향을 주지 않는다. 하지만 스레드의 경우에는 code,data,heap메모리 내용을 공유하기 때문에 스레드 하나에서 오류가 발생하면 같은 프로세스 내 스레드가 강제종료된다.

멀티스레딩 장점과 단점(싱글스레드 vs 멀티스레드)

  • 장점 : context switching시 공유하고 있는 메모리 만큼의 메모리 자원을 아낄 수 있고, 스레드는 프로세스 내 stack영역을 제외한 모든 메모리 영역을 공유하기 때문에 통신의 부담이 적어 응답속도가 빠르다(자원 공유하므로 적게 사용하기도). : 병렬성,응답성,경제성
  • 단점 : 스레드 하나 오류로 전체 프로세스가 종료될 수 있고, 자원을 공유하기때문에 동기화 문제가 발생할 수 있다.

반대로 멀티프로세스는 안정성이 확보되고 동기화가 별도로 필요하지않지만,conetxt switching 비용이 크고 프로세스간 통신이 복잡하다.
그러므로 시스템 특징에 따라 적절한 선택이 필요

  • 착각하면 안되는 부분 : concurrency


    실제 동시에 처리되는 것이 아닌 빠른 context switching으로 한 작업이 끝나기 전에 실행이 가능하다는 의미.

멀티프로세스와 멀티스레드일 수도 있고, 멀티스레드 싱글 프로세스일 수도 있고, 싱글 프로세스 싱글 스레드일 수도 있고 멀티프로세스 싱글스레드일 수도 있다. 멀티프로세스 멀티스레드 > 멀티프로세스 싱글스레드 > 싱글프로세스 멀티스레드 > 싱글프로세스 싱글스레드 순으로 간단해지고 경량화.(context switch비용이 적음)

  • 싱글스레드
    멀티프로세스 여부를 떠나서, 단순히 CPU만을 사용하는 계산작업이라면 멀티스레드보다 싱글스레드로 프로그래밍하는 것이 cost도 적게 들고 효율적이다.

    • 싱글스레드는 context switching 비용이 없다
    • 자원 동기화를 신경쓰지 않아도 된다.
    • 그러나 여러개의 CPU를 활용하지 못한다
      • 프로세서를 최대한 활용하려면 cluster 모듈을 이용하거나 외부에서 여러 프로그램 인스턴스를 실행시키는 방법을 사용해야한다(멀티프로세스), 이 때 통신에 대한 문제는 Redis같은 도구를 이용해 해결.
    • 연산량이 많은 작업은 작업이 완료될 때 까지 다른 작업들이 대기해야한다.
  • 멀티스레드 서버
    server-client 사이 멀티스레드. 클라이언트가 서버에게 요청을 보내면 서버는 새로운 스레드 하나를 생성해 요청을 수행한다. 서버는 기본적으로 request queue를 가지고있고, iterative server는 main process가 request queue에서 응답을 처리하는 방식으로 작동하지만 concurrent 서버는 요청이 들어올 때 마다 main process 를 fork하여 응답을 처리했었다. 그러나 fork연산은 비용이 크므로 multithread를 활용해 더 빠른 concurrent 처리가 가능하다.

  • 싱글 서버 ex- 장고
    장고는 default로는 single threaded 프로그램이다. 또한 요청을 순차적으로 처리한다.(요청이 끝나기 전에 다른 요청은 block- 기본적으로 동기화 방식이다)
    장고는 web server gateway interface(WSGI)라는 표준 인터페이스를 사용해 웹서버 요청을 처리한다.
    서버는 어플리케이션을 실행하고 환경과 콜백함수를 함께 제공하며 어플리케이션은 요청을 처리하고 제공된 콜백함수를 이용해 서버에 응답을 반환한다.
    이 때 WSGI가 실행하는 multithread를 통해 동시성이 제공됨.
    장고는 싱글스레드이지만, WSGI에서 멀티스레드 서버를 제공하는 듯 한다. wsgi가 멀티 스레드를 만들 수 있도록 하여 request 요청이 많아지더라도 효율적으로 처리할 수 있게 한다. (장고는 동기방식,싱글스레드나 실행 환경 gunicorn등을 이용해 여러개 스레드(프로세스도) 에서 장고를 실행시켜 많은 리퀘스트를 처리할 수 있게 한다)

  • 싱글스레드 서버 ex- nodejs
    nodejs는 기본적으로 철학이 비동기방식이다.
    정확하게는 javaascript 실행 환경인 nodejs는 사실 여러개 스레드를 가지고있다.
    그러나 자바스크립트를 실행하는 스레드는 이벤트 루프 단 하나이므로 싱글 스레드 기반이라고 한다. (Single Threaded Event Loop Model architecture)

    node는 이벤트 기반의 플랫폼이기 때문에 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식으로 작동하고, nodejs에서 일어나는 모든 처리는 일련의 콜백을 처리하는 것과 같다고 할 수 있다. node는 v8라는 자바스크립트 엔진과 비동기 작업을 처리하는 libuv라는 C++로 만들어진 라이브러리로 이루어져있다.
    장고처럼 nodejs 어플리케이션은 단일 cpu코어(싱글스레드)에서 실행되나 클러스터 모듈을 사용해서 마스터 프로세스에서 여러 워커 프로세스를 생성해서 모든 코어를 사용하게끔 하기도 한다(pm2 이용)
    결국 장고와 노드js는 어플리케이션 자체는 싱글스레드로 유사한듯. 다만 nodejs는 비동기를 기본 정책으로, 장고는 동기를 기본 정책으로 함.

multiocore programming

  • 다중코어 : core가 여러개의 cpu 칩 형태(혹은 cpu칩 안에 여러 개 core,processor)가 존재하는 것.(multiprocessor, 다중처리기)
    동시성이란 여러개의 스레드를 번갈아가며 동시 실행인 것 처럼 보이게 하는 방식(concurrency)이고, 병렬성이란 멀티코어시스템에서 사용되는 개념으로 여러 개 코어가 각 스레드를 실제 동시 수행하는 것(parallelism)을 말한다.

multithreading models

user thread, kernel thread?

  • User thread
    사용자 레벨 thread lib에 의해 kernel 지원 없이 관리(pthread, win32). 사용자가 생성하는 스레드.
    user 영역에서 스레드 연산을 수행하며, 커널에 의존적이지 않음. 스케줄링 결정이나 동기화를 위해 커널을 호출하지 않기 때문에 인터럽트가 발생할 때 커널 레벨 스레드보다 오버헤드가 적다.(사용자영역 스레드에서 행동하기 때문에 OS scheduler의 context switching이 없음, 커널은 사용자레벨 스레드 존재조차 모름)
    but user level만 사용했을 때에는 os가 thread에 직접 interrupt 전달 불가해 preemptive 스케줄링 불가능하고 하나의 thread때문에 모든 process 내 thread가 block된다. (스케줄링 우선순위를 지원하지 않고, 프로세스에 속한 스레드 중 I/O작업 등에 하나라도 블록이 걸린다면 전체 스레드가 블록)
  • Kernel thread
    커널 자체에서 제공하는 thread. hw의 자원을 활용하기 위해 존재.
    하나의 프로세스는 적어도 하나의 커널 스레드를 가지며 커널스레드들로는 다른 스레드가 입출력 작업이 끝날 때 까지 다른 스레드 작업이 가능하지만(커널 스케줄링, 커널이 각 스레드들 개별 관리가능) 커널 호출 하는 것이 무겁고 오래걸리고, (context switching) 사용자모드에서 커널모드로 전환이 빈번해 성능저하.
  • User Thread + Kernel thread
    user thread + kernel level thread를 함께 사용.
    OS 입장에서는 kernel thread를 따로 만들어 관리하고, user thread와 연결될 때 user thread를 수행할 수 있도록 한다. (한정된 자원)
    interrupt가 발생할 시 user level의 thread scheduler에게 어떤 user level thread가 받아야하는지 알려준다. 또한 blocking system call 시(read() 등) 해당 thread의 수행은 중단시키고 새로운 kernel stack을 할당해 userprocess에게 붙인 다음 리턴하는 방식으로 user level만 사용했을 때 단점 개선.

user thread - kernel thread 대응 모델

  • Many to One (pure user-level)
    kenel 1개 당 user thread n개 (1:n)

    하나의 kernel thread에 접근하기 위해 user thread들이 경쟁해야한다. multicore system이라고 해도 kernel thread가 1개이므로 병렬로 수행이 불가능하고 user thread1개가 kernel thread 독점할 수도 있다.
    I/O가 사용자 스레드에서 하나라도 발생한다면 해당 프로세스는 I/O가 풀릴 때 까지 block되고 다른 작업을 할 수 없다.(동시성 지원이 거의 의미없음)
  • One to One (pure kernel-level)

    사용자 스레드들을 하나의 커널 스레드로 매핑.
    1:1방식이기에 병렬성은 좋으나 효율성이 떨어지고, OS자원이 한정적이므로 thread에 제한이 필요하다. blocking 일어나지 않음. 그러나 커널 호출(user-kernel모드전환)에 오버헤드가 클수있다.
  • Many to Many(combined)

    여러 개 사용자 스레드를 여러 개 커널 스레드로 매핑. 커널 스레드는 생성된 사용자 스레드 수 이하로 생성되어 스케줄링한다. 커널이 user와 kernel 스레드 매핑을 적절하게 조절해 concurrent하면서도 성능도 어느정도 보완한다. lwp(경량프로세스:사용자와 커널스레드 사이 중간 자료구조)라는 것이 kernel thread가 어떤 user thread와 연결할지 정해준다.
  • two -level
    many to many에서 응용 thread가 정말 중요하다고 생각되면 blocking되지 않도록 하나의 kernel thread와 연결시켜줄 수 있는 모델

thread libraries

  • 커널 지원 없이 사용자 공간(지역)에서만 라이브러리를 제공하는 방법. 함수 호출은 그냥 사용자가 정의한 지역 함수를 호출하는 것.
  • OS에서 지원하는 kernel 공간에서 구현. library api를 호출하는 것은 kernel system call을 호출하는 것.
  • thread는 라이브러리에서 생성하고 없앤다. ex) pthread_create() / pthread_join(wait)/ pthread_exit()/ , cpu가 실행시켜주는 thread는 kernel level thread이다. 리눅스에서는 kernel 레벨의 POSIX Pthreads 라이브러리를 사용한다.
    예시코드 - 출처 velog 기운찬곰

    #include <stdio.h>
    #include <pthread.h>  //라이브러리 사용
    
    int thread_args[3] = {0,1,2};
    
    //함수이지만 스레드로 동작
    //void * : 주소를 리턴. 
    void *Thread(void *arg){  
      int i;
    
      for(i=0; i<5; i++){
        printf("thread %d: %dth iteration\n", *(int*)arg,i);
      }
      pthread_exit(0); //스레드 종료
    }
    
    void main(void){
      int i;
      pthread_t threads[3];  // 우리가 생성할 스레드를 위한 식별자
    
      for(i=0; i<3; i++){
         printf("Thread Num %d Create!!\n", i);
        //스레드 생성. Thread실행 -> 인자로는 thread_args[i]의 주소
        pthread_create(&threads[i],null,(void*(*)(void *))Thread, &thread_args[i]);
      }
    
      pthread_exit(0);
    }


동기스레딩, 비동기스레딩

(1) 동기 쓰레딩(Synchronous threading)

  • 부모 thread가 하나 이상의 자식 thread를 생성하고 자식 thread가 모두 종료할 때까지 기다렸다가 자신의
    실행을 재개하는 방식을 말한다. 이 방식은 '포크-조인(fork-join)' 전략이라고 불린다.
  • 부모가 생성한 Thread들은 병행하게(concurrently) 실행되지만 부모는 자식들의 작업이 끝날 때까지
    실행을 계속할 수 없다.
  • 동기 쓰레딩(Synchronous threading)은 상당한 양의 데이터를 공유하게 된다.
  • 예를 들어, 부모 thread는 자식들이 계산한 결과를 통합할 수 있어야 한다.

(2) 비동기 쓰레딩(Asynchronous threading)

  • 부모 thread가 자식 thread를 생성한 후, 부모는 자신의 실행을 재개하여 두 thread를 병행하게 실행한다.
  • 각 Thread는 모든 다른 Thread와 독립적으로 실행하기 때문에, 부모 thread는 자식의 종료를 알 필요가 없다.
  • 각 Thread들은 독립적이기 때문에, thread들 사이의 데이터 공유는 거의 없다.

implicit threading

explicit thread: 프로그래머에 의해 생성되고 수행되는 스레드(직접 thread를 생성하고 사용)
Implicit thread : thread의 생성과 관리를 프로그래머가 아닌 컴파일러와 런타임 라이브러리에서 해준다.(thread의 생성과 관리를 시스템에서 해주고 application은 thread를 쉽게 활용할 수 있게 library에서 제공)

thread pool

  • 매번 thread가 생성되는 낭비를 방지하기 위해 사용된다. thread를 미리 준비해놓고 작업 시 쉬고있는 thread를 사용한다. (생성 시간을 줄일 수 있고 concurrent threading이 가능-kernel)
  • library가 시작 시 thread를 여러 개 만들어놓고 필요 시 할당해준다. 사용 후 thread pool로 돌아간다. 하나의 application에서 사용할 수 있는 thread에 제한을 줄 수도 있음. 시스템에서 생성과 관리를 담당하여 user는 task에 집중이 가능.
  • grand central dispatch(GCD) : 동적으로 thread pool 크기를 바꿔주는 구조. serial queue와 concurrent queue가 존재. concurrency queue에 low,default,high의 3개 큐가 존재해 thread가 동시에 수행되도록 한다. ( application이 블록 객체 형태로 작업 전송 할 수 있는 대기열을 제공,관리하며 thread클래스 대신 사용하는 개념. queue에 전달된 작업은 시스템이 전적으로 관리하는 스레드풀에서 실행되고 , 앱을 실행하면 시스템이 자동으로 메인스레드 위에서 동작하는 Main 큐(Serial Queue)를 만들어서 작업을 수행하고, 그 외에 추가적으로 여러 개의 Global 큐(Cuncurrent Queue)를 만들어서 큐를 관리. 동기/비동기 방식 실행이 가능하지만 main queue에서는 비동기(async)만 사용가능하다.)

threading issues

Fork() 및 Exec() system call:

Q) 멀티스레드프로그램에서 만일, 한 프로그램의 Thread가 fork()를 호출하면, 새로운 프로세스는 모든 Thread를
복제해야 하는가?한 개의 Thread만 가지는 프로그램이어야 하는가?( 프로세스 통째로 복사 vs thread만 복사)
A) OS에 따라 다르다. exec은 똑같이 동작.

신호처리(signal handling):

  • Signal은 UNIX에서 프로세스에게 사건 일어남 알려주기 위해 사용/ event의 source나 reason에 따라 동기/비동기식으로 전달.
    1. 신호는 특정 이벤트에의해발생.
    2. 프로세스에게 전달된다.
    3. default/사용자정의 시그널핸들러에의해처리.

    - 동기식 신호(Synchronous signal)의 예로는 잘못된 메모리 접근, 0으로 나누기 등이 있다.
    - 비동기식 신호(Asynchronous signal)의 예로는. (Ctrl+C)와 같은 키를 눌렀을 때가 해당된다.
    - 동기적 신호는 해당 신호를 발생시킨 Thread에게 전달되어야 하고, 다른 Thread에게는 전달되면 안 된다.
    - 비동기적 신호는 누가 신호를 발생시켰는지 명확하지 않으므로, 프로세스 내 모든 Thread에게 신호가 전달된다
    멀티스레드에서 시그널처리방법
    - 시그널적용 스레드에만전달/ 프로세스내모든스레드에전달/프로세스내스레드선택적전달/특정스레드가모든시그널받도록할당

취소 (Cancellation)

  • 스레드 취소(Thread Cancellation)은 Thread가 끝나기 전에 그것을 강제 종료시키는 작업을 말한다.
  • 예를 들어, 여러 Thread들이 데이터베이스를 병렬로 검색하고 있다가 한 Thread가 원하는 결과를
    찾게 되면 나머지 Thread들은 취소되어도 된다.
  • 이처럼 취소되어할 Thread들을 목적 스레드(Target Thread)라고 부른다.
  • 취소(Cancellation)은 아래 두 가지와 같은 방식으로 발생할 수 있다.
    (1) 비동기식 취소(Asynchronous cancellation) : 한 Thread가 즉시 Target Thread를 강제 종료시킨다.
    (2) 지연 취소(Deferred cancellation) : Target Thread가 주기적으로 자신이 강제 종료되어야 할지 점검한다.
  • 지연취소 사용 이유
    Thread 취소를 어렵게 만드는 것은 취소 Thread에게 할당된 Resource의 문제이다. process와 다르게 같은 프로세스 내 thread는 자원을 공유하기때문.
    또한, Thread가 다른 Thread와 공유하는 자료구조를 갱신하는 과정에서 취소 요청이 와도 문제가 될 수 있다. (lock을 걸어놓은 변수가 있을수도)
    이에, 비동기식으로 Thread를 취소하면 필요한 시스템 자원을 다 사용 가능한 상태로 만들지 못할 수도 있다.(ciritical section에 target thread가 작업하고 있을 시 혹은 공유자원 자용 시 죽이게 되면 문제)
    지연 취소의 경우, Thread는 자신이 취소되어도 안전하다고 판단될 때 취소 여부를 검사할 수 있다.
  • Cancellation의 Default 유형은 지연 취소(Deferred Cancellation)이다.

Thread-Local Storage

  • 한 프로세스에 속한 Thread들은 그 프로세스의 data를 모두 공유한다.
    그러나, 상황에 따라 각 Thread는 자기만 Access 할 수 있는 데이터를 가져야 할 필요가 있을 수 있다.
    이러한 data를 스레드 국지 저장소(Thread-Local Storage, TLS)라고 부른다.
  • TLS를 local variable과 혼동하기 쉽다.
  • Local Variable이 하나의 함수가 호출되는 동안에만 보이는 반면에 TLS는 전체 함수 호출에 걸쳐 보인다.
  • TLS는 static data와 유사한데, 차이점은 TLS data는 각 Thread의 고유한 정보를 저장한다는 점이다.

스케줄러 액티베이션(Scheduler Activation)

  • Multithread Program에서는 Thread Library와 kernel의 통신 역시 고려해야 한다.
  • 이 통신은 Many-to-Many, Two-level Model에서 반드시 해결해야 하는 문제이다.
  • 이러한 통신을 통해 Kernel Thread의 수를 동적으로 조절하여 Application이 최고의 성능 보이도록 보장할 수 있다.
  • Many-to-Many 또는 Two-level model을 구현하는 많은 시스템들은 user와 kenrel thread 사이에
    중간 자료 구조를 둔다. 이 자료구조는 보통 lightweight process, or LWP라고 불린다.
  • LWP는 하나의 kenrel thread에 속해있으며, kernel thread가 block 되면 LWP 역시 함께 block 된다.​
  • User Thread Library와 kernel Thread 간의 통신 방법 중의 하나는 Scheduler Activation이다.
  • Scheduler Activation은 아래와 같이 동작한다.
    (1) kernel은 application에게 virtual processor(LWP)의 set을 제공한다.
    (2) Application은 virtual processor로 user thread를 schedule 한다.
    (3) kernel은 application에게 certain event에 대해 알려줘야 한다. (이를 'upcall'이라고 한다.)!
profile
정선용

0개의 댓글