SHIN JI HAN 2025. 5. 12. 01:20
728x90
728x90
Reporting Date: April. 30, 2025

스레드에 대해 다루고자 한다.


목차

01 스레드 라이브러리
02 P 스레드
03 Windows 스레드
04 JAVA 스레드
05 암묵적 스레드


 

 

 

01 스레드 라이브러리

Thread Library

프로그래머가 스레드 생성 및 관리할 수 있도록
API
를 제공하는 소프트웨어 구성 요소로,
일반적으로 사용자 공간에 존재한다.

이러한 라이브러리는 구현 방식에 따라 크게 두 가지로 나눌 수 있다.

 OS의 커널 지원 없이 전적으로 사용자 공간에서 구현

스레드 라이브러리의 코드와 자료 구조가 모두 사용자 공간에 위치하며,
라이브러리 함수 호출은 시스템 콜이 아닌 일반 함수 호출로 처리된다.

따라서 시스템 콜에 따른 오버헤드가 줄어드는 장점이 있지만,
커널의 지원이 없으므로 여러 스레드를 동시에 병렬로 실행하는 데에는 한계가 있다.

 

OS가 직접 스레드를 관리하는 커널 수준의 구현

스레드 관련 자료 구조와 코드가 커널 공간에 존재하며,
API 호출 시 시스템 콜을 통해 커널이 스레드를 생성하고 관리한다.

진정한 병렬 처리가 가능하다는 장점이 있지만,
시스템 콜로 인한 오버헤드가 발생한다.

현재 널리 사용되는 스레드 라이브러리는 다음과 같다:

  • Win32 스레드 (Windows 기반)
  • Java 스레드 (Java 가상 머신 기반)
  • POSIX Pthread (유닉스 및 리눅스 기반)

이들은 각 플랫폼과 언어 환경에 맞춰
스레드 생성, 동기화 등의 기능을 제공한다.


 

02 P 스레드

POSIX threads OR Pthreads
Portable OS Interface with X (from UNIX)

POSIX(IEEE 1003.1c) 표준에서 정의한 API로,
스레드의 생성과 동기화를 위한 인터페이스를 제공한다.

이는 특정한 구현체를 지칭하는 것이 아니라,
스레드 관련 기능을 어떻게 제공해야 하는지를 규정한
명세(specification)이다.

따라서 각 OS 개발자는 이 표준을 바탕으로 자율적으로 구현할 수 있다.

 

주로 UNIX 계열 OS에서 사용되며,
Solaris, Linux, macOS, Tru64 UNIX 등 다양한 시스템에서 널리 채택되고 있다.

반면, Windows OS는 기본적으로 Pthreads를 지원하지 않지만,
Pthreads-w32와 같은 공개 라이브러리를 이용하면
일부 POSIX 스레드 API를 사용할 수 있다.

이 라이브러리는 Windows 환경에서 POSIX 스레드 기능의
일부를 구현해 주며, redhat.com 등에서 제공된다.

 

하나의 프로세스 내에서 여러 스레드가 메모리를
공유하는 구조를 기반으로 동작한다.

이를 통해 스레드 간 통신이 빠르고 효율적으로 이루어질 수 있으나,
동시에 상호 배제(mutex)나 데드락과 같은 동기화 문제
적절히 관리하는 것이 중요하다.


 

03 Windows 스레드

Windows에서 스레드 생성 시 윈도우 스레드 라이브러리를 사용한다.
이 방식은 여러 측면에서 POSIXPthread 방식과 유사하다.

' 1 : 1 매핑 (One-to-One Mapping) '  방식을 채택하고 있어,
사용자 수준의 각 스레드마다 커널 수준의 스레드가 하나씩 대응된다.

이로 인해 각 스레드는 OS 커널에 의해 개별적으로 스케줄링되며,
진정한 병렬 실행이 가능하다.

 

핵심 구성 요소

먼저, 각 스레드를 고유하게 식별할 수 있도록 Thread ID가 부여된다.

또한, 프로세서의 상태를 나타내는 레지스터 집합(Register Set)이 존재하며,
실행 중인 스레드가 사용하는 두 종류의 스택이 있다.

사용자 모드에서 실행될 때 사용하는 사용자 스택(User Stack)과
커널 모드에서 동작할 때 사용하는 커널 스택(Kernel Stack)이다.

여기에 더해, 여러 라이브러리에서 활용할 수 있는
스레드 전용의 데이터 저장 영역 Private Data Storage Area도 포함된다.

 

이러한 레지스터 집합, 스택, 데이터 저장 영역 등을 모두 합쳐
스레드의 '문맥(Context)'
이라고 부른다.

이 문맥은 스레드의 실행 상태 보존 및 전환에 중요한 역할을 한다.


 

04 JAVA 스레드

Java는 프로그래밍 언어 수준에서 스레드의
생성과 관리를 직접 지원하는 특징을 가지고 있다.

자바에서 스레드를 만드는 방법은 크게 두 가지로 나뉘며,
각각의 방식은 상황에 따라 적절히 선택된다.

 

 1 .  새 클레스 정의

Thread 클래스를 상속받아 새로운 클래스를 정의하는 것이다.

Thread() 생성자나 Thread(String name) 생성자를
이용해 스레드를 생성할 수 있다.

상속을 통해 run() 메서드를 오버라이딩하면,
해당 메서드에 정의된 작업이 스레드 실행 시 수행된다.

 

 2 .   Runnable 인터페이스 구현

특히 해당 클래스가 이미 다른 클래스를 상속받고 있는 경우 유용하다.

Java는 단일 상속만 지원하므로, 다중 상속이 필요한 상황에서는
Runnable 인터페이스를 구현하여 스레드를 생성하는 것이 일반적이다.

Runnable 인터페이스는 run()이라는 추상 메서드를 포함하고 있으며,
이 메서드에 실행할 코드를 정의한다.

그런 다음 Thread(Runnable r)
또는 Thread(Runnable r, String name)
생성자를
사용해 스레드를 생성할 수 있다.

 

자바 프로그램은 기본적으로 하나 이상의 제어 흐름, 즉 스레드를 포함하며,
이러한 스레드들은 모두 자바 가상 머신(JVM)에 의해 생성되고 관리된다.

JVM은 각 스레드의 실행과 종료, 스케줄링 등을 책임지며,
OS의 커널 수준 스레드를 기반으로 자바 스레드를 구현한다.

이처럼 자바 스레드는 언어와 JVM이 직접 제공하고 지원하는 기능으로,
멀티스레딩 프로그래밍을 비교적 쉽게 구현할 수 있게 해준다.

 


 3 .   Java 예제

Runnable 인터페이스를 구현하여 스레드를 만들고,
MutableInteger라는 사용자 정의 클래스를 통해
공유 데이터를 스레드 간에 전달하려는 구조.

// 가변 정수 값을 저장하는 클래스
class MutableInteger {
    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

// Runnable을 구현한 Summation 클래스
class Summation implements Runnable {
    private int upper;
    private MutableInteger sumValue;

    public Summation(int upper, MutableInteger sumValue) {
        this.upper = upper;
        this.sumValue = sumValue;
    }

    @Override
    public void run() {
        int sum = 0;
        for (int i = 0; i <= upper; i++) {
            sum += i;
        }
        sumValue.setValue(sum); // 결과 저장
    }
}

// 테스트용 main 클래스
public class ThreadExample {
    public static void main(String[] args) {
        MutableInteger result = new MutableInteger();
        Summation task = new Summation(100, result);
        Thread thread = new Thread(task);
        thread.start();

        try {
            thread.join(); // 스레드 완료 대기
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("1부터 100까지의 합: " + result.getValue());
    }
}

 MutableInteger는 스레드 간 공유할 수 있는 객체로,
setValue()와 getValue() 메서드를 통해 값을 설정하고 읽을 수 있다.

 Summation 클래스는 Runnable을 구현하여,
run() 메서드에서 1부터 upper까지의 합을 계산하고, sumValue에 저장한다.

main에서는 Thread 객체를 생성하고 시작한 뒤
join()을 통해 스레드가 끝날 때까지 기다린다.

 


 

 4 .   Java 예제

// 명령행 인자를 받아, 해당 숫자까지의 합을 계산 및 출력하는 프로그램.

public class Driver {
    public static void main(String[] args) {
        if (args.length > 0) {
            int upper = Integer.parseInt(args[0]);

            if (upper < 0) {
                System.err.println(args[0] + " must be >= 0.");
            } else {
                // 공유 객체 생성
                MutableInteger sum = new MutableInteger();
                
                // 합산 작업을 위한 스레드 생성 및 시작
                Thread thrd = new Thread(new Summation(upper, sum));
                thrd.start();

                try {
                    thrd.join();  // 스레드가 종료될 때까지 기다림
                    System.out.println("The sum of " + upper + " is " + sum.getValue());
                } catch (InterruptedException ie) {
                    ie.printStackTrace();
                }
            }
        } else {
            System.err.println("Usage: java Driver <non-negative integer>");
        }
    }
}

앞서 정리했던 두 클래스(MutableInteger, Summation)도
같은 프로젝트에 포함되어 있어야 이 Driver 클래스가 정상 작동한다.

 

실행 방법

javac Driver.java MutableInteger.java Summation.java
java Driver 100

 

 

// 예시 출력 결과물
The sum of 100 is 5050

 


 

 5 .   상태 및 전이 조건

이 표는 자바의 Thread 클래스와 JVM 스레드 스케줄링 모델을 기반으로 작성된 것이다.

상대 설명 전이 조건
New (생성) new 키워드로 스레드 객체가 생성된 상태. start() 호출 시 → Runnable
Runnable (실행 가능) 실행 준비가 완료된 상태로, CPU 할당을 기다림. CPU 할당 시 → Running sleep(),
I/O 대기 시 → Blocked 또는 Waiting
Running (실행 중) CPU를 할당받아 run() 메서드를 실행하는 상태. run() 종료 시 → Terminated sleep()
또는 I/O 등으로 대기 시 → Blocked
Blocked (차단) 잠금(lock)을 기다리거나 I/O 등으로 인해 차단된 상태. 자원 또는 조건 충족 시 → Runnable
Waiting (대기) 명시적으로 무기한 대기 중인 상태
(wait(), join() 등 호출).
notify(), join() 완료 등으로 조건 충족 시
Runnable
Timed Waiting (시간 제한 대기) 일정 시간 동안 대기하는 상태
(sleep(ms), wait(ms) 등).
시간이 만료되거나 조건 충족 시
Runnable
Terminated (종료) run() 메서드가 종료되어 실행이 끝난 상태. -

 


 

자바에서 멀티스레딩을 활용한 동시성 프로그래밍 패턴 중 하나.

생산자(Producer)소비자(Consumer) 간의 작업을
공유 자원(버퍼)을 통해 비동기적으로 처리하는 구조이다.

public class Factory {

    public Factory() {
        // First create the message buffer.
        Channel mailBox = new MessageQueue();

        // Create the producer and consumer threads and pass
        // each thread a reference to the mailBox object.
        Thread producerThread = new Thread(new Producer(mailBox));
        Thread consumerThread = new Thread(new Consumer(mailBox));

        // Start the threads.
        producerThread.start();
        consumerThread.start();
    }

    public static void main(String[] args) {
        Factory server = new Factory();
    }
}

 


 

import java.util.Date;

class Producer implements Runnable {
    private Channel mbox;

    public Producer(Channel mbox) {
        this.mbox = mbox;
    }

    public void run() {
        Date message;
        while (true) {
            // nap for a while
            SleepUtilities.nap();

            // produce an item and enter it into the buffer
            message = new Date();
            System.out.println("Producer produced " + message);
            mbox.send(message);
        }
    }
}

 


 

import java.util.Date;

class Consumer implements Runnable {
    private Channel mbox;

    public Consumer(Channel mbox) {
        this.mbox = mbox;
    }

    public void run() {
        Date message;
        while (true) {
            // nap for a while
            SleepUtilities.nap();

            // consume an item from the buffer
            message = (Date) mbox.receive();
            if (message != null) {
                System.out.println("Consumer consumed " + message);
            }
        }
    }
}

 

05 암묵적 스레드

Implicit Threading

멀티코어 환경에서 스레드 수가 증가함에 따라,
개발자가 직접 스레드를 명시적으로 생성 및 관리하는 방식은

코드의 복잡성을 높이고 검증을 어렵게 만든다.

이를 해결하기 위해 스레드의 생성과 관리를 컴파일러
또는 런타임 라이브러리에게 위임하는 방식
이 도입되었다.

즉, 스레드 관련 작업을 자동화함으로써
응용 프로그램 개발자의 부담을 줄이고,
병렬 처리를 보다 안정적으로 수행할 수 있게 한다.

 

 1 .   스레드 풀

Thread Pool

스레드를 무한정 생성하면 시스템의 CPU 시간,
메모리, 스레드 스케줄링 오버헤드 등 자원이 고갈될 수 있다.

이를 방지하기 위한 대표적인 암묵적 스레딩 기법이다.

프로세스 시작 시 일정 수의 스레드를 미리 생성해 풀에 보관해두고,
작업 요청이 들어올 때마다 기존 스레드를 재사용
하는 방식이다.

이 접근법은 스레드 생성과 종료에 드는 비용을 줄이고,
응답 속도 향상과 시스템 자원의 효율적 사용에 기여한다.

 


 

 2 .   OpenMP

Open Multi-Processing

 

C, C++FORTRAN 언어를 위한 병렬 프로그래밍 API로,
주로 공유 메모리 환경에서 멀티스레드 프로그래밍을 지원한다.

코드에 삽입하는 컴파일러 지시어(Directive)를 통해
병렬 처리를 간편하게 구현할 수 있도록 설계되었다.

 

핵심 개념은 코드 중에서 병렬 실행이 가능한 부분을
병렬 영역(Parallel Region)으로 명시하고,

해당 영역에 #pragma omp parallel과 같은
컴파일러 지시어를 삽입하여 병렬 처리를 수행하는 것이다.

이 지시어를 만나면, 시스템의 코어 수만큼 스레드가 생성되어
병렬 영역의 작업을 동시에 실행한다.

예를 들어, 배열 덧셈 연산을 병렬화하는 코드는 다음과 같다:

#pragma omp parallel for
for (int i = 0; i < N; i++) {
    c[i] = a[i] + b[i];
}

이처럼 #pragma omp parallel for는 루프 내 작업을
여러 스레드에 분산시켜 병렬로 처리하게 하며, 

멀티코어 환경에서 성능 향상을 기대할 수 있다.

 

또한 컴파일러가 소스 코드를 분석하여 지시어에 따라 기계어로 변환하면서,
개발자가 직접 스레드 생성이나 동기화를 명시하지 않아도 되도록 도와준다.

예를 들어, 4개의 코어가 있는 쿼드코어 시스템이라면 하나의 작업을
4개의 스레드가 분할 처리하여 실행 시간을 단축할 수 있다.

 


 

 

#include <omp.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    /* sequential code */

    #pragma omp parallel
    {
        printf("I am a parallel region.\n");
    }

    /* sequential code */
    return 0;
}

 

OpenMP 병렬 영역에서 각 스레드가 "I am a parallel region."을 출력하게 된다.
OpenMP가 제대로 작동하려면 컴파일 시 -fopenmp 옵션을 사용해야 한다.

// 예시
gcc -fopenmp filename.c -o output

 


 

 3 .  GCD

Grand Central Dispatch

애플의 macOS iOS에서 지원하는 병렬 프로그래밍 기술로,
C, C++, Objective-C 언어에서 사용할 수 있도록 API
실행 시간 라이브러리를 제공한다.

이 기술은 개발자가 병렬로 실행 가능한 코드 블록을 정의할 수 있도록 지원하며,
복잡한 스레드 생성 및 관리 작업을 OS가 대신 처리한다.

개발자는 병렬로 실행할 코드 영역만 식별하면 되며,
그 외의 세부적인 스레딩 처리는 GCD가 자동으로 수행한다.

GCD에서 병렬로 실행할 코드는 ^{ ... } 구문으로 정의되며,
이 코드는 블록(Block)이라 불린다.

// 예시
^{ printf("I am a block"); }

이처럼 작성된 블록은 발송 대기열(Dispatch Queue)에 등록되고,
OS는 이를 적절한 시점에 스레드 풀 내의 가용한 스레드에 할당하여 실행한다.

 

발송 대기열의 종류

Dispatch Queue

두 가지 유형의 발송 대기열을 제공한다:

① 직렬 큐, Serial Queue

큐에 추가된 작업을 FIFO(선입선출) 순서로 하나씩 실행한다.

기본적으로 앱마다 하나의 메인 큐(Main Queue)가 있으며,
개발자가 원하는 만큼 별도의 직렬 큐를 생성할 수도 있다.

 

동시 큐, Concurrent Queue

블록은 FIFO 순서로 큐에서 제거되지만,
동시에 여러 블록이 병렬로 실행될 수 있다.

 

시스템 제공 대기열 및 서비스 품질

QoS; Quality of Service

시스템 전역에서 사용할 수 있는 4가지 QoS 등급의 대기열을 제공한다.

이들은 작업의 중요도와 응답 속도 요구에 따라 다음과 같이 구분된다:

QoS 클래스 용도 / 작업 예시 우선순위
QOS_CLASS_USER_INTERACTIVE 사용자 인터페이스(UI) 업데이트 등 즉각적인 반응이 필요한 작업 매우 높음
QOS_CLASS_USER_INITIATED 사용자가 명시적으로 요청한 작업 (예: 문서 열기, 버튼 클릭 반응 등) 높음
QOS_CLASS_UTILITY 짧지 않은 계산 작업, 주기적인 백그라운드 작업 (예: 다운로드, 처리 등) 중간
QOS_CLASS_BACKGROUND 사용자와의 직접적인 상호작용이 없는 작업 (예: 백업, 색인 작업 등) 낮음

이처럼 GCD는 병렬 프로그래밍의 복잡성을 줄이고,
앱의 성능을 향상시키기 위한 고수준의 프레임워크를 제공한다.


[교제] 운영체제 제 10


728x90
반응형