Ch.4 Threads & Concurrency

imagine·2025년 3월 13일

OS

목록 보기
5/6

Motivation

  • 대부분의 어플은 여러 작업을 동시에 처리해야 하기 때문에 멀티스레드를 사용하는 것이 일반적이다.
  • 스레드는 어플 내에서 동작하며, 서로 독립적으로 실행된다.
  • 멀티테스크는 분리된 스레드로 구현된다. 어플 내의 여러 작업을 별도의 스레드로 구현할 수 있다
    • 디스플레이 업데이트
    • 데이터 패치
    • spell checking
    • 네트워크 요청에 응답
  • 프로세스 생성은 무겁지만 스레드 생성은 가볍다. 프로세스를 생성하는 것은 비용이 많이 들지만, 스레드를 생성하는 것은 비교적 가볍다. 따라서 멀티스레드를 사용하면 애플리케이션 내에서 더 빠르고 효율적으로 다중 작업을 처리할 수 있다.
  • 코드를 단순화하고 효율성을 높일 수 있다.
  • 커널은 일반적으로 멀티스레드이다.

Overview

Process : 실행중인 프로그램

각각의 프로세스는 실행에 필요한 리소스를 차지(occupy)한다.

Thread : 프로그램이 동시에 실행되는 두 개 이상의 작업으로 분할하는 방법

  • CPU 사용률의 기본 단위
  • 프로세스보다 작은 단위
  • 프로세스 내의 thread는 리소스를 공유한다

스레드의 구성 : 스레드ID, 프로그램 카운터, 레지스터 집합, 스택 등등

Single and Multithreaded Processes

single thread = 1프로세스 1스레드. / multi thread = 1프로세스 여러스레드

Thread Control Block (TCB)

스레드 제어 블록(TCB)은 운영 체제 커널 내에서 관리하기 위해 필요한 스레드별 정보를 담고 있는 데이터 구조이다.

TCB에 포함될 수 있는 정보의 예시

  • 스레드 ID
  • 스레드의 상태 (실행 중, 준비 중, 대기 중, 시작, 완료 등)
  • 스택 포인터
  • 프로그램 카운터
  • 스레드의 레지스터 값
  • 프로세스 제어 블록(PCB)에 대한 포인터

이러한 정보는 각 스레드의 상태 및 실행에 필요한 모든 정보를 제공하여 스레드를 효과적으로 관리할 수 있도록 합니다. 예를 들어, 스레드의 상태를 추적하고 스레드 간의 전환을 수행하는 데 필요한 정보를 제공합니다. 또한 스레드가 실행되는 동안 프로그램 카운터 및 레지스터 값을 추적하여 스레드가 중단되고 다시 시작될 때 이전 상태로 복원할 수 있습니다.

Why Thread?

프로세스 생성은 시간 및 리소스 측면에서 비용이 많이 든다.

ex) 웹 서버는 수천개의 요청을 받는다

Benefits

  • 응답성 (responsiveness) : 프로세스의 일부가 차단되거나 긴 작업을 수행하는 경우 프로그램이 계속 실행하는 것을 허용함으로써, 사용자에 대한 응답성을 증가시킨다.
  • 자원 공유 (resource sharing) : 스레드는 프로세스의 리소스를 공유하므로 shared memory / message passing(IPC)보다 쉽다.
  • 경제성 (economy) : 프로세스 생성보다 저렴하고, context switching보다 thread switching이 오버헤드가 낮다.
  • 규모 적응성 (scalability) : 프로세스는 멀티프로세서 architecture의 이점을 갖는다. (다중 프로세서 구조에서 각각의 스레드는 다른 프로세서에서 병렬로 수행가능)

Multicore Programming

프로그래머가 Multicore 또는 multiprocessor 시스템을 구현하기 위해서는 다음과 같은 도전과제를 극복해야 한다.

  • 활동 분할(dividing activities)
  • 균형(balance)
  • 데이터 분할(Data splitting)
  • 데이터 종속성 (data dependency)
  • 테스팅 & 디버깅 (testing and debugging)

Parallelism(병렬화) : 시스템이 하나 이상의 작업을 동시에 수행할 수 있음을 의미한다

Concurrecncy(동시성) : 진행 중인 두 가지 이상의 작업을 지원한다

Concurrency vs. Parallelism

Concurrency - 하나 이상의 작업이 모두 다 조금씩 진행 (단일코어에서)

Parallelism - 완전히 동시에 실행 (멀티코어에서 가능)

Types of Parallelism

  • Data parallelism : 동일한 데이터의 하위 집합을 여러 코어에 배포하고 각 코어에서 동일한 작업을 수행한다.
    • (e.g. 100만개의 데이터 더하기 (연산 하나), 각 코어가 동일 연산 수행)
  • Task parallelism : 각 스레드가 고유한 작업을 수행하는 여러 코어에 스레드 분산한다.
    • (e.g. 4가지의 다른 종류의 통계연산 실행, data는 동일하지만 각 core마다 다른 연산 수행)

Amdahl’s Law

직렬 및 병렬 구성 요소가 모두 있는 어플리케이션에 추가 코어를 추가하여 성능 향상을 식별한다.

  • S : 병렬 불가능한 부분, N : 코어 개수

    ex) 어플리케이션이 75% 병렬, 25% 직렬인 경우 1코어에서 2코어로 이동하면 속도가 1.6배 빨라진다

    2코어에서 4코어로 이동하면 속도가 1.6배에서 2.28배로 빨라진다

  • N이 무한대에 가까워지면 속도는 1/S에 가까워진다.

  • 어플리케이션의 직렬부분은 추가 코어를 추가함으로써 얻을 수 있는 성능에 불균형적인 영향을 미친다.


Multithreading Models

User Threads and Kernel Threads

  • User threads : user-level 스레드 라이브러리에 의해 지원 및 관리된다.
    • 라이브러리 함수 호출에 의해 생성된다(시스템 콜 X)
    • 커널이 관련되지 않는다
    • 사용자 스레드의 전환은 커널 스레드의 전환보다 빠르다
  • Kernel hread : 커널에서 직접 지원 및 관리한다.
    • 커널에 의해 생성되고 관리된다
    • 커널에 의해 스케줄된다
    • 프로세스보다 싸다
    • 사용자 스레드보다 비싸다

대부분의 운영 체제 커널은 일반적으로 멀티 스레드이다. windows, Linux, macOS를 포함한 거의 모든 최신 운영 체제 → 커널 스레드 지원

각 스레드는 장치 관리, 메모리 관리, 인터럽트 핸들링과 같은 특정 작업을 수행한다

Multithreading Models

주요 이슈 : user thread-kernel thread 사이의 통신(correspondence)

세가지 멀티스레딩 모델들

  • Many-to-One
  • One-to-One
  • Many-to-Many
  • Two-level

1. Many-to-One

단일 kernel 스레드에 많은 user-level 스레드가 매핑된다.

(-) 하나의 스레드가 차단되면 모두 차단된다.

(-) 한 번에 하나의 스레드만 커널에 있을 수 있으므로 다중 스레드는 multicore 시스템에서 병렬로 실행되지 않을 수 있다.

  • 현재 이 모드를 사용하는 시스템은 거의 없다.

2. One-to-One

리눅스에서 많이 사용 - 심플!

각 user-level 스레드가 kernel 스레드에 매핑된다.

user-level 스레드를 생성하면 kernel 스레드가 생성된다.

(+) Many-to-One보다 더 많은 동시성

(-) 오버헤드로 인해 때때로 프로세스 당 스레드 수가 제한된다.

  • 대부분의 OS(Linux, Windows)가 이 방식을 사용한다.

3. Many-to-Many

다수의 user-level 스레드를 다수(더 작거나 같은 수)의 kernel 스레드로 매핑한다.

(+) OS가 충분한 수의 kernel 스레드를 생성할 수 있도록 한다

num(user threads) >= num(kernel threads)

4. Two-level Model

Many-to-Many와 비슷하지만, user 스레드를 kernel 스레드에 바인딩할 수도 있다.

num(user threads) >= num(kernel threads) && One-to-One model 가능


Thread Libraries

스레드 라이브러리(thread library)는 프로그래머에게 스레드 생성 및 관리를 위한 API를 제공한다.

구현하는 두 가지 주요 방법

  • user-level 라이브러리 (커널 지원 없이 완전히 사용자 공간에 있는)
  • OS에서 직접 지원하는 kernel-level 라이브러리

Pthreads

user-level 또는 kernel-level로 제공될 수 있다.

스레드 생성 및 동기화를 위한 POSIX 표준 API

사양(specification), 구현 X(not implementation)

API는 스레드 라이브러리의 동작을 지정하며, 구현은 라이브러리의 개발에 달려있다.

UNIX OS에서 공통적으로 사용된다. (Solaris, Linux, Mac OS X) ????????????????

  • pthread_create() : 새 스레드를 생성한다

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

    프로세스를 호출해서 새 스레드를 시작한다

    thread : 성공했을 때의 새 스레드 ID

    attr : 스레드 특성 설정. 기본값은 NULL

    start_routine : 스레드가 생성될 때 실행할 루틴

    arg : 루틴을 시작하기위해 argument 전달

    return 값 - 성공 : 0 / 실패 : error number

  • pthread_join() : 종료된 스레드에 연결한다.

    지정한 스레드가 돌아올 때까지 호출 스레드를 차단한다

    상위 스레드가 반환되면 모든 하위 스레드가 종료된다.

    //ex) pthreads code_10개의 스레드를 연결하는
    #define NUM_THREADS 10
    
    //an array of threads to be joined upon
    pthread_t workers[NUM_THREADS];
    
    for(int i=0; i<NUM_THREADS; i++)
    	pthread_join(workers[i], NULL);
  • pthread_cancel() : 스레드에 취소 요청을 보낸다

Java Threads

자바 스레드는 JVM에서 관리한다.

일반적으로 기본 OS에서 제공하는 스레드 모델을 사용하여 구현된다

자바 스레드는 다음에 의해 생성될 수 있다.

  • Extending Thread class (스레드 클래스 확장)
  • Implementing the Runnable interface (runnable 인터페이스 구현)
  • 코드
    //Runnable 인터페이스 구현
    
    public interface Runnable {
    	public abstract void run();
    }
    • 스레드 생성 Thread worker = new Thread(new Task()); worker.start();
    • 스레드 대기 중 try { worker.join(); } catch (InterruptedException e){ }

자바는 명시적으로 스레드를 생성하는 대신, 실행자 인터페이스를 중심으로 스레드를 생성할 수 있다.


Implicit Threading

암묵적 스레딩(Implicit threading) : 프로그래머가 아니라 컴파일러와 런타임 라이브러리에 의해 스레드가 생성 및 관리되는 것

스레딩의 생성과 관리 책임을 프로그래머→컴파일러와 실행시간 라이브러리에게 넘겨주는 Implicit threading을 사용하면 멀티 스레딩을 효율적으로 사용할 수 있다.

세가지 방법

  • Thread pools
  • Fork Join model
  • OpenMP

기타 : Grand Central Dispatch, Intel Thread Building Blocks (TBB) 등등

1. Thread Pools

작업 대기 중인 풀에 여러 개의 스레드 생성

(+) 일반적으로 새 스레드를 만드는 것보다 기존 스레드로 요청을 처리하는 속도가 약간 더 빠르다

(+) 어플리케이션의 스레드 수를 풀 크기로 바인딩할 수 있다

(+) 수행할 작업을 생성하는 메커니즘과 분리하면 작업을 실행하기 위한 다양한 전략을 사용할 수 있다

i.e. 작업이 시간 지연 후에 실행되거나 정기적으로 실행되도록 예약할 수 있다.

1-2. Java Thread Pools

java.util.concurrent package 자바 실행 프레임워크에서 지원된다.

thread pool을 만드는 세가지 방법

  • static ExecutorService newSingleThreadExecutor()
    • 크기 1의 pool을 생성
  • static ExcutorService newFixedThreadPool(int size)
    • 지정된 스레드의 개수만큼 pool 생성
  • static ExcutorService newCachedThreadPool()
    • 많은 인스턴스를 재사용하여 무제한의 스레드 pool 생성

+code

2. Fork Join Parallelism

여러개의 스레드(작업)를 fork한 다음 결합(join)한다

ForkJoinTask는 추상적인 기본 클래스이다.

RecursiveTask, RecursiveAction 클래스는 ForkJoinTask를 확장한다.

  • RecursiveTask : 는 결과를 반환한다.
  • RecursiveAction : 결과 반환X

+code

3. OpenMP

shared-memory(IPC) 환경에서 병렬 프로그래밍을 지원한다.

  • C, C++, FORTRAN용 컴파일러 지시문 및 API 세트이다.
  • 병렬 영역(parallel regions) 식별 : 병렬로 실행할 수 있는 코드 블록

코어 수만큼 스레드를 생성한다.

#pragma amp parallel

루프를 병렬로 실행한다.

+code


Threading Issues

fork() and exec()

멀티스레드 프로세스에서 fork()

멀티스레드 프로세스에서 exec()

  • 모든 스레드를 포함한 전체 프로세스 교체

Thread Cancellation

스레드 취소의 문제점

  • 스레드는 다른 스레드와 리소스를 공유한다. 프로세스에는 고유한 리소스가 있다 → 스레드가 다른 스레드와 공유된 데이터를 업데이트하는 동안 스레드를 취소할 수 있다.

두 가지 일반적인 접근 방식

  • 비동기 취소(asynchronous cancellation) : 하나의 스레드가 타겟 스레드를 즉시 종료한다.
  • 연기된 취소 (deferred cancellation) : 타겟 스레드가 종료해야 하는지 주기적으로 확인한다. (terminate할 수 있는 상황인지 주기적으로 check)

thread cancellation을 호출하면 취소가 요청되지만, 실제 취소는 스레드의 상태에 따라 다르다.

스레드 취소가 비활성화(disabled)되어 있으면 스레드가 활성화(enabled)할 때까지 취소가 보류상태로 유지된다.

기본 타입은 deferred이다.

  • 스레드가 취소 지점 (cancellation point)에 도달한 경우에만 취소가 발생한다.
    • 즉, pthread_testcancel()
    • 그 후 정리 처리기(cleanup handler)가 호출된다.

리눅스 시스템에서 스레드 취소는 신호(signal)를 통해 처리된다.

Thread-Local Storage

프로세스에서, 모든 스레드는 전역 변수를 공유한다.

Thread-local storage(TLS)를 사용하면 각 스레드가 자체 데이터 복사본을 가질 수 있다.

__thread int tls; //on pthread

각 스레드에는 고유한 ‘int tls’ 변수가 있다

지역 변수와 다르다

  • 단일 함수 호출 중에만 표시되는 지역 변수
  • 함수 호출 전반에서 볼 수 있는 TLS

정적 데이터와 유사하다. 그러나 TLS는 각 스레드마다 고유하다.

스레드 생성 프로세스를 제어하지 못할 때 (스레드 pool을 사용할 때) 유용하다.

Scheduler Activations

M:M 및 Two-level 모델 모두 어플리케이션에 할당된 적절한 수의 커널 스레드를 유지하기 위해 통신이 필요하다.

일반적으로 사용자 스레드와 커널 스레드 간에 중간 데이터 구조를 사용한다 - lightweight process (LWP)

  • light weight process(LWP)
    • 프로세스가 사용자 스레드를 실행하도록 예약할 수 있는 가상 프로세서이다.
    • 커널 스레드마다 LMP가 달려있다.
  • 스케줄러 활성화(Scheduler activation)*upcalls을 제공한다. *upcall: 커널에서 스레드 라이브러리의 upcall handler로의 통신 메커니즘

이 통신을 통해 어플리케이션은 올바른 수의 커널 스레드를 유지할 수 있다.


signal

신호 : 특정 이벤트가 발생했음을 프로세스에 알리기 위해 UNIX에서 제공하는 메커니즘

  • 신호는 다양한 소스에 의해 생성된다.
  • 신호는 프로세스에 전달된다.
  • 프로세스가 처리한다.
    • default signal handler (kernel)
    • User-defined signal handler

신호의 타입

  • 동기식(synchronous) : 동일한 프로세스로부터의 신호 ex) 불법 메모리 접근, 0으로 나눗셈
  • 비동기식(asynchronous) : 외부 소스로부터의 신호 ex) Ctrl + C

신호 기질/성향 (signal disposition) (신호와 관련된 동작)

  • 각 신호는 현재의 disposition이 있으며, 이는 신호가 전달될 때 프로세스가 어떻게 동작하는지를 결정한다.
  • 특정 신호가 발생할 때마다 호출되는 기능을 제공할 수 있다. → 이 기능을 신호 처리기(signal handler)라고 한다.
  • 신호 disposition을 SIG_IGN으로 설정하여 신호를 무시할 수 있다.
  • 신호 disposition을 SIG_DFL로 설정하여 default disposition을 설정할 수 있다.

signal 2

signal() 신호와 관련된 작업을 정의한다

sigaction() 신호 작업을 검사 및 변경한다

+code

signal Handling

신호를 어떤 스레드로 전달해야 하나?

가능한 옵션

  • 신호가 적용되는 스레드에 신호 전달
  • 프로세스의 모든 스레드에 신호 전달
  • 프로세스의 특정 스레드에 신호 전달
  • 모든 신호를 수신할 특정 스레드를 할당 → 신호의 종료에 따라 다르다

다른 방식 : 신호를 전달할 스레드를 지정한다.

ex) pthread_kill(did, signal) in POSIX


Operating System Examples

Linux Threads

리눅스는 thread보다 task라고 말한다.

스레드 생성은 clone() 시스템 콜을 통해 이루어진다

clone()을 사용하면 자식 태스크가 부모 태스크(프로세스)의 주소 공간을 공유할 수 있다.

  • flags control behavior

struck task_struct는 프로세스 데이터 구조(공유 또는 고유)를 가리킨다.

(출처)
Operating System Concepts 도서
https://www.booksfree.org/operating-system-concepts-10th-edition-by-abraham-silberschatz-peter-b-galvin-greg-gagne-pdf/

profile
공부하는 기록

0개의 댓글