Computer Science/시스템 프로그래밍

Chapter 12. POSIX Threads

만능 엔터테이너 2024. 11. 26. 10:13
728x90
반응형
SMALL

Motivation

{파일 디스크립터 모니터링} - concurrent하게 실행되는 스레드 예시

기본적으로 프로그램을 컴파일하여 프로그램을 실행하면 프로세스가 만들어지고 프로세스는 기본값으로 1개 이상의 스레드가 반드시 생성됨 -> 스레드가 실행 컨텍스트 정보를 가지고 있음

 

[프로세스] a와 b라는 프로그램을 생성하여 각각 따로 실행하면 빠르게 스위치해가면서 2개의 프로세스가 동시에 실행되는 것처럼 실행됨 => [스레드]  한 프로세스 내부에 a라는 스레드와 b라는 스레드가 실행되는 동안 스레드 간의 스위치가 되어 concurrent하게 진행됨

  • 별도 프로세스 : 자식 프로세스는 어떤 변수도 공유하지 않음
  • select(), poll() : 블로킹 호출 - 싱글 스레드 기반으로 서버를 구축하는 방법, 타이머를 설정해 두어 일정 기간 동안 반환되지 않으면 자동으로 반환함 [많이 사용함]
  • 폴링을 이용한 논블로킹 I/O : 때로는 I/O 체크를 위한 타이밍을 하드코딩(얼마나 자주해야되는지 타이밍을 고려해야 하는 단점이 존재)해야 함, 읽을 게 있으면 읽어오고 읽을 게 없으면 다음 파일을 모니터링 작업을 수행 및 반환을 함
  • POSIX 비동기 I/O : 핸들러는 오직 async-signal-safe 함수만 사용 
  • 별도 스레드 : 다른 접근 방식보다 간단함

다중 스레드를 왜 사용하는가?

  • 스레드를 사용해 프로그래밍해야 하는 이유는 많습니다.(다중 스레드를 concurrent하게 실행하여 performance를 향상시킴), 이 수업의 맥락에서 중요한 2가지는 다음과 같습니다:
    • 스레드는 비동기 이벤트를 효율적으로 처리할 수 있게 해줍니다. (언제 클라이언트가 메시지를 보낼 지 모를 경우, 이러한 상황을 위해서 각각의 상황에 대한 전담 스레드를 생성하여 만들어두고 다른 스레드는 다른 작업을 수행하도록 설정)
    • 스레드는 공유 메모리 멀티프로세서에서 병렬 성능을 얻을 수 있게 해줍니다. 
  • 스레드는 운영 체제를 작성하는 데 큰 도움이 됩니다. -> 다중 스레드를 사용하게 되면 프로그램 간의 동기화 문제를 고려해야 함!

 

  • 각각의 스레드는 실행 단위(또는 실행 흐름 )이며, 스택과 CPU 상태(즉, 레지스터)로 구성됩니다.
  • 다중 스레드는 다음과 유사합니다:
    • 다중 프로세스와 비슷하지만 프로세스는 메모리를 공유하지 않고 각각의 독립적인 메모리 공간을 사용함 But, 하나의 작업 내에서 다중 스레드동일한 코드, 전역 변수, 힙을 이러한 메모리들을 서로 공유하며 사용합니다.
  • 따라서, 유닉스에서 두 프로세스는 운영 체제(예: 파일, 파이프, 소켓)를 통해서만 통신할 수 있는 반면,
    • 하나의 작업 내 두 스레드는 메모리를 통해 통신할 수 있습니다.
  • 스레드로 프로그래밍할 때, 스레드가 동시에 실행된다고 가정합니다. 실제로는 싱글 CPU일 경우 concurrent하게 수행됨, 다중 CPU일 경우 실제로 동시에 실행됨
    • 다시 말해, 마치 각 스레드가 자신만의 CPU에서 실행되고, 모든 스레드가 동일한 메모리를 공유하는 것처럼 보입니다.

멀티태스킹

  • 단일 프로세서에서 멀티스레딩은 일반적으로 시간 분할 다중화 (멀티태스킹에서처럼) 를 통해 발생합니다:
    • 프로세서는 다른 스레드 간을 전환합니다.
    • 이 컨텍스트 전환은 사용자에게 스레드나 태스크가 동시에 실행되는 것처럼 느껴질 정도로 자주 발생합니다.
  • 멀티프로세서나 멀티코어 시스템에서는,
    • 스레드나 태스크가 실제로 동시에 실행됩니다.
    • 각 프로세서나 코어는 특정 스레드 또는 태스크를 실행합니다.
  • 많은 현대 운영 체제는 두 가지를 직접 지원합니다:
    • 프로세스 스케줄러를 사용한 시간 분할 및 멀티프로세서 스레딩
    • 운영 체제 커널은 프로그래머가 시스템 호출 인터페이스를 통해 스레드를 조작할 수 있도록 지원합니다.

프로세스와 스레드

  • 스레드는 전통적인 멀티태스킹 운영 체제의 프로세스와 구별됩니다. 프로세스는 다음과 같은 특징이 있습니다:
    • 일반적으로 독립적입니다. (프로세스가 사용하는 메모리 공간은 os에 의해서 기존의 메모리 공간과 겹치지 않도록 관리를 해줌, 기본적으로 2개의 프로세스는 서로 다른 메모리 공간을 사용하기 때문에 메모리를 침범하지 않음)
    • 상당한 상태 정보를 보유합니다. (스레드 보다 관리해야할 상태 정보가 많음)
    • 개별 주소 공간을 가집니다. (그렇기에 프로세스 간 통신을 하기 위해서는 아래처러 프로세스 간 통신 메커니즘이 필요함 -> os 도움을 받아야 프로세스가 통신이 가능함)
    • 시스템에서 제공하는 프로세스 간 통신 메커니즘을 통해서만 상호작용합니다.

=> 스레드는 이 특징들의 반대 개념! (1개의 스레드가 os 도움 없이 메모리를 공유하여 통신이 가능함)

프로세스

  • 프로세스는 커널 스케줄링의 가장 무거운(heaviest) 단위입니다.
  • 프로세스는 운영 체제에서 할당된 리소스를 소유합니다.
    • 리소스에는 메모리, 파일 핸들, 소켓, 장치 핸들, 창 등이 포함됩니다.
  • 기본적으로 프로세스는 주소 공간이나 파일 리소스를 공유하지 않습니다.
    • 단, 파일 핸들 상속, 공유 메모리 세그먼트, 동일 파일의 공유 매핑과 같은 명시적 방법을 통해서는 예외입니다.
  • 프로세스는 일반적으로 선점형 멀티태스킹 방식(프로세스가 cpu를 가지고 실행 중이다가 우선순위 등의 경우로 os는 스케줄링 정책 상 쫓겨나고 더 우선순위가 높은 프로세스가 교체되어 실행되는 방식)으로 처리됩니다.

다중 스레드

  • 일반적으로 프로세스의 상태 정보공유(code, global variable, static, heap 등등)하며, 메모리와 기타 리소스를 직접 공유합니다.
  • 동일 프로세스 내 스레드 간 컨텍스트 전환은 프로세스 간 컨텍스트 전환보다 빠릅니다.

프로세스 vs 스레드

  • 스레드는 커널 스케줄링의 가장 가벼운(lightest) 단위입니다.
  • 각 프로세스 내에는 최소한 하나의 스레드가 존재합니다.
  • 하나의 프로세스 내 여러 스레드가 존재할 경우, 이들은 같은 메모리와 파일 리소스를 공유합니다.
  • 운영 체제의 프로세스 스케줄러가 선점형일 경우, 스레드 선점형 멀티태스킹 방식으로 동작합니다.
  • 스레드는 리소스를 소유하지 않습니다.
    • 단, 스택, 레지스터 복사본(프로그램 카운터 포함), 스레드 로컬 저장소(있을 경우)는 예외입니다.
  • 커널 스레드유저 스레드 간에는 구분이 있습니다:
    • 커널 스레드는 커널에서 관리 및 스케줄링됩니다.
    • 유저 스레드는 사용자 공간에서 관리 및 스케줄링됩니다.

=>  딱히 구분하지 않음

사용자 공간(User Space)

  • 전통적인 운영 체제는 보통 가상 메모리(프로그램을 실행하면 실행시킬 정보를 하드디스크가 메모리로 로드하여 메모리에 적재된 정보를 커널이 읽어서 실행한는데, 가상 메모리는 전부가 아닌 필요한 부분만 로드를 하고 지금 당장 필요가 없는 공간은 하드 디스크를 나가기도 함 / 스왑인 - 디스크가 메모리를 로드, 스왑아웃 - 디스크가 메모리를 나감)커널 공간(Kernel Space)과 사용자 공간(User Space)으로 분리합니다.
  • 커널 공간
    • 커널, 커널 확장, 일부 디바이스 드라이버 실행을 위해 엄격히 예약됩니다.
    • 대부분의 운영 체제에서 커널 메모리는 디스크로 스왑되지 않습니다.
  • 사용자 공간(스왑인과 스왑아웃이 일어나는 공간)
    • 모든 사용자 모드 애플리케이션이 작동하는 메모리 영역입니다.
    • 필요할 경우 이 메모리는 스왑될 수 있습니다.

왜 Pthreads (Posix Threads)인가?

  • 아래 표는 fork() 서브루틴과 pthread_create() 서브루틴의 실행 시간 비교를 보여줍니다.
    • 50,000개의 프로세스/스레드 생성을 기준으로 합니다.
    • 시간 단위는 초(seconds)이며, 최적화 플래그는 사용하지 않았습니다.

 

 

 

POSIX 스레드 함수들

  • pthread_cancel : 다른 스레드를 종료합니다.
  • pthread_create : 스레드를 생성합니다.
  • pthread_detach : 리소스를 해제하도록 스레드를 설정합니다.
  • pthread_equal : 두 스레드 ID를 비교합니다.
  • pthread_exit : 프로세스를 종료하지 않고 스레드를 종료합니다.
  • pthread_kill : 특정 스레드에 신호를 보냅니다.
  • pthread_join : 스레드를 대기합니다.
  • pthread_self : 자신의 스레드 ID를 확인합니다.
  • 대부분의 함수는 성공 시 0을 반환하며, 실패 시 0이 아닌 오류 코드를 반환합니다.
    • errno를 설정하지 않습니다.
  • POSIX 스레드 함수 중 어떤 것도 EINTR을 반환하지 않으며, 중단되었을 경우 다시 실행할 필요가 없습니다.

ID로 스레드 참조하기

#include <pthread.h>
pthread_t pthread_self(void);
int pthread_equal(pthread_t t1, pthread_t t2);​

 

 

  • pthread_self : 자신의 ID를 확인합니다.
  • pthread_equal
    • pthread_t는 구조체일 수 있습니다.
    • 두 ID가 같으면 0이 아닌 값을 반환하며, 그렇지 않으면 0을 반환합니다.

스레드 생성하기

#include <pthread.h>
int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, 
                   void *(*start_routine)(void *), void *restrict arg);​

 

 

  • 별도의 시작 작업 없이 스레드를 자동으로 실행 가능 상태로 만듭니다.
  • 매개변수
    • thread: 새로 생성된 스레드의 ID입니다.
    • attr: 속성 객체를 나타냅니다. 기본 속성의 경우 NULL을 사용합니다.
    • start_routine: 실행할 함수의 이름입니다.
    • arg: start_routine에서 사용하는 단일 매개변수입니다.
  • 반환값 : 성공하면 0을 반환하고(스레드 생성 및 함수를 시작한 게 아니고, 스레드를 만들거고 함수를 실행할 건데 지금 당장은 아닐지라도 언젠가는 실행을 한 거라는 것 / os가 요청을 성공적으로 받아들임), 실패하면 0이 아닌 값을 반환합니다.

분리(Detaching)

#include <pthread.h>
int pthread_detach(pthread_t thread);

 

 

pthread_detach

  • 스레드가 분리 상태(detached thread)가 아니면 (datach <-> joinable), 종료 시 리소스를 해제하지 않습니다.
  • 이 함수는 스레드가 종료될 때 스레드의 저장 공간을 회수할 수 있도록 내부 옵션을 설정합니다.
  • 분리된 스레드는 종료 시 상태를 보고하지 않습니다.
  • 성공하면 0을 반환하고, 실패하면 0이 아닌 값을 반환합니다.

조인(Joining)

#include <pthread.h>
int pthread_join(pthread_t thread, void **value_ptr);

 

  • pthread_join
    • 호출한 스레드를 대상 스레드가 종료될 때까지 대기 상태로 만듭니다.
    • 분리되지 않은(non-detached) 스레드의 리소스는 다른 스레드가 pthread_join을 호출하거나 전체 프로세스가 종료되기 전까지 해제되지 않습니다.
  • 매개변수
    • thread: 대상 스레드
    • value_ptr: 반환 상태에 대한 포인터의 위치. 상태를 가져오지 않으려면 NULL을 사용합니다.
  • 반환값 : 성공하면 0을 반환하고, 실패하면 0이 아닌 값을 반환합니다.
  • 예시 : 아래 코드는 교착 상태를 발생시킵니다. (이렇게 사용하면 안됨!)
pthread_join(pthread_self());

 

 

monutorfd - 파일 디스크립터 여러 개를 모니터 함수로 모니터링할 수 있는 다중 스레드 함수

tid - 배열의 인덱스를 생성

for문을 사용하여 pthread_create 함수로 생성하고 순차적으로 pthread_join함수로 대기 및 실행 과정을 반복하여 메인 함수를 종료하는 예제

스레드 종료

#include <pthread.h>
void pthread_exit(void *value_ptr);

 

pthread_exit

  • 호출한 스레드를 종료시킵니다.
  • 프로세스의 exit()와의 차이점:
    • pthread_exit는 호출한 스레드만 종료시키고, 프로세스는 계속 실행됩니다.
  • return 문은 암묵적으로 pthread_exit를 호출합니다.
  • value_ptr: pthread_join 호출을 통해 다른 스레드에서 반환 값을 사용할 수 있습니다.

스레드 취소 (Cancellation)

#include <pthread.h>
int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state, int *oldstate);

 

 

pthread_cancel

  • 다른 스레드의 취소를 요청합니다.
  • 호출한 스레드를 차단하지 않고 취소 작업이 완료되도록 합니다.
  • 성공하면 0을 반환하고, 실패하면 0이 아닌 값을 반환합니다.
  • 결과는 대상 스레드의 상태와 유형에 따라 달라집니다:
    • PTHREAD_CANCEL_ENABLE: 요청을 즉시 처리합니다.
    • PTHREAD_CANCEL_DISABLE: 요청을 대기 상태로 둡니다.
  • pthread_setcancelstate
    • 호출한 스레드의 취소 가능 상태를 변경합니다.
    • 매개변수:
      • state:
        • PTHREAD_CANCEL_ENABLE: 취소 요청 허용.
        • PTHREAD_CANCEL_DISABLE: 취소 요청 대기.
      • oldstate: 이전 상태를 저장하는 변수.
    • 성공하면 0을 반환하고, 실패하면 0이 아닌 값을 반환합니다.

취소 유형 (Cancellation type)

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
void pthread_testcancel(void);

 

 

  • 취소의 어려움
    • 스레드가 종료 전에 해제해야 하는 리소스를 보유하고 있는 경우 문제가 발생할 수 있습니다.
    • 종료 핸들러에서 리소스를 해제하는 것이 항상 실현 가능한 것은 아닙니다.
  • 취소 유형
    • 스레드가 취소 요청에 응답하여 종료할 지점을 제어할 수 있습니다.
    • PTHREAD_CANCEL_ASYNCHRONOUS : 스레드는 언제든지 요청에 응답할 수 있습니다.
    • PTHREAD_CANCEL_DEFERRED : 스레드는 지정된 취소 지점에서만 요청에 응답합니다. (사용자가 원하는 시점에 요청을 응답하게끔 설정)
  • pthread_setcanceltype : 스레드의 취소 유형을 변경합니다.
  • pthread_testcancel : 특정 코드 위치에서 취소 지점을 설정하기 위해 이 함수를 호출합니다.

 

매개변수 전달과 값 반환

  • 다중 매개변수
    • 생성자는 배열이나 구조체에 대한 포인터를 사용해야 합니다.
    • Program 12.4 : 두 개의 열린 파일 디스크립터를 포함하는 배열을 전달합니다.
  • 값 반환
    • Program 12.5
      • 복사된 총 바이트 수를 반환하기 위해 메모리 공간을 할당합니다.
      • 호출한 프로그램은 이 공간을 해제해야 합니다.
      • 만약 바이트 수가 static 저장 클래스에 있는 변수에 저장되고 이 static 변수에 대한 포인터가 반환된다면?
        • 하나의 스레드에서는 정상적으로 동작합니다. (메인 스레드에서 copy하여 여러 개의 스레드를 만듦, 이때, 스레드 간의 충돌 문제 (스레드 간 동기화) 부분이 필요하게 됩니다.
        • 두 개의 스레드에서는 둘 다 동일한 위치에 바이트 수를 저장하게 됩니다. (사용 불가능!)
    • Program 12.4 및 12.5의 문제점
      • 스레드는 다른 스레드가 아니라 자신이 사용한 자원을 정리해야 합니다.
      • 단일 정수를 저장하기 위해 동적으로 공간을 할당하는 것은 비효율적입니다.
    • 대안 접근법
      • 생성 스레드는 반환 값 공간에 대한 포인터인자 매개변수로 전달합니다. (넘겨 줄 때, 반환 값을 받을 배열의 공간을 1개 더 추가하여 보내줌, ex) 배열이 2일 경우, 배열의 크기를 3으로 넘겨줌 -> 배열을 받으면 just 복사하기만 하면 됨) 
      • Program 12.6 및 12.7
        • 매개변수는 크기 3의 배열입니다.
        • 세 번째 요소는 복사된 바이트 수를 저장합니다.
        • 반환 값은 배열을 통해 얻거나 pthread_join의 두 번째 매개변수를 통해 얻을 수 있습니다.

pthread_t 타입의 NUMTHREADS를 10으로 정의되어 있어서, 10개의 스레드를 저장하고 for문을 돌아서 0부터 9까지 반복하면서 pthread_create 함수를 통해서 스레드를 생성하고, printarg함수를 통해서 새로 생성된 스레드는 화면에 int값을 출력하게 됨 (이때, 넘겨지는 int값은 for문의 i값을 넘겨주는 것) 첫 번째 스레드에는 i가 0인 값이 넘어가고, 두 번째 스레드에는 i가 1인 값이 넘어가고 이렇게 각각 9번째까지 값을 넘기고 스레드를 종료시킴 / 10개의 스레드가 전부 출력이 되고 나면 main 스레드가 "All threads done"을 출력하고 0을 반환하여 종료함

 

 

Program 12.9

  • 10개의 스레드를 생성하며, 각 스레드는 넘겨받은 매개변수 값을 출력합니다. (스레드는 생성될 때마다 다른 값을 가짐)
  • 스레드 생성 루프의 인덱스 i가 매개변수로 전달됩니다.

  • 결과는 무엇인가?
    • 결과는 시스템이 스레드를 스케줄링하는 방식에 따라 달라집니다.
    • 다중 스레드를 생성할 때, 스레드가 매개변수를 읽는 작업을 마칠 때까지 해당 매개변수를 보관하는 변수를 재사용하지 않아야 합니다.

{스레드 솔루션}

  • sleep(1)의 조건을 추가해서 1초 이내 동안 스레드는 생성된다고 가정하여, 1초마다 스레드가 1개씩 생성하도록 변경 (이렇게 실행된다고 하면 스레드를 생성할 때마다 스레드가 순차적으로 생성이 가능함, 무작위로 생성x) -> But, 완벽한 해결책이라고는 어려운 점이 1초마다 스레드가 생성되지 않고 과부하가 걸릴 수 있음

 스레드 안정성(Thread Safety)

  • 스레드 안전 함수(Thread-safe function)
    • 여러 스레드가 동시에 활성 상태로 함수 호출을 실행해도 상호 간섭이 발생하지 않는 함수입니다.
    • POSIX는 표준 C 라이브러리의 함수들을 포함하여 모든 필수 함수들이 스레드 안전 방식으로 구현되도록 명시합니다. 
728x90
반응형
LIST