Operation System/Computer System

니피Operation System/Computer System

왜 여러 개의 스레드 풀이 하나의 스레드 풀보다 좋은가?

하나의 큰 스레드 풀을 사용하는 것은 마치 "전화 상담원이 10명 뿐인 콜센터"와 같습니다. 이 콜센터는 모든 종류의 전화를 다 받습니다.

  • 짧고 중요한 전화 : VIP 고객의 긴급 문의 (예 : 빠른 API 응답)

  • 길고 덜 중요한 전화 : 일반 고객의 장시간 하소연 (예 : 대용량 파일 다운로드, 외부 API 호출, 복잡한 DB 쿼리)

문제 상황 시나리오 :

  1. 10명의 상담원이 모두 일반 고객의 장시간 하소연에 붙잡혀 있다고 가정하자.

  2. 이때 VIP 고객이 전화를 걸었다.

  3. 하지만 모든 상담원이 통화 중이므로, VIP 고객은 다른 전화가 끝날 때까지 계속 기다려야 한다.

  4. 결과적으로 , 1초면 끝날 수 있었던 중요한 작업이 30초짜리 다른 작업 때문에 지연된다. 애플리케이션 전체의 반응성이 떨어지고, 사용자는 서비스가 매우 느리다고 느끼게 된다.

이 현상을 Head-of-Line Blocking이라고 부른다. 이 때 스레드 풀을 두 개 , VIP 전용 콜센터와 일반 고객 창구로 나눠서 3: 7로 배분하면 다음과 같은 장점이 생긴다.

  1. 작업 격리(Isolation) 및 안정성 확보

    • 장애 전파 방지: 만약 일반 고객 창구(스레드 풀 B)에서 처리하는 외부 API 호출 작업이 모두 지연되어 7개의 스레드가 전부 멈춰버려도, VIP 고객 창구(스레드 풀 A)의 3개 스레드는 아무런 영향을 받지 않습니다.

    • 핵심 기능 보호: 이 덕분에 오래 걸리거나 실패 가능성이 있는 작업이 애플리케이션의 핵심 기능(빠른 API 응답 등)을 마비시키는 것을 막을 수 있습니다. 즉, 시스템 전체의 안정성과 가용성(Availability)이 크게 향상됩니다.

  2. 리소스 최적화 및 성능 향상

  • CPU - intensive 작업 : (예 : 암호화, 데이터 압축) : 스레드 개수를 CPU 코어 수에 가깝게 설정하여 컨텍스트 스위칭 비용을 최소화하고 CPU 효율을 극대화 한다.

  • I/O - intensive 작업 : (예 : DB 조회, 외부 API 호출) : 스레드가 대기(Blocking) 상태에 있는 시간이 길기 때문에, 스레드 개수를 CPU 코어 수보다 훨씬 많게 설정하여 CPU 가 쉬는 동안 다른 스레드가 일할 수 있도록 한다.

  • 하나의 스레드 풀로는 이런 세밀한 튜닝이 불가능하다.

  1. 우선순위 제어

  • 중요한 작업(예 : 결제 처리) 를 위한 스레드 풀과 덜 중요한 작업 (예 : 알림 발송, 로그 기록) 을 위한 스레드 풀을 분리하여, 시스템 리소스가 항상 중요한 작업에 우선적으로 할당되도록 제어할 수 있다.

예시

  • Netty (비동기 이벤트 기반 프레임워크): * Boss Group (스레드 풀): 클라이언트의 최초 연결 요청(accept)만 처리하는 스레드 풀. 매우 빠르고 절대 블로킹되지 않아야 합니다. (보통 스레드 1개) * Worker Group (스레드 풀): 연결된 클라이언트와의 실제 데이터 I/O(read/write)를 처리하는 스레드 풀. (보통 CPU 코어 수 * 2)

  • Tomcat:

    • 요청을 받는 스레드 풀과, 서블릿을 실행하는 스레드 풀을 분리하여 관리합니다.

동시성과 병렬성

동시성은 작업들이 시간을 나누어 사용하여 마치 동시에 실행되는 것처럼 보이게 하는 반면, 병렬성은 실제로 여러 작업을 동시에 실행한다.

동시성은 작업의 실행 순서를 잘 조정하여, 자원의 효율적 사용과 빠른 응답 시간을 달성할 수 있다. 이는 특히 I/O 작업이 많은 웹 서버나 데이터베이스 서버에서 유용하게 사용된다.

병렬성은 여러 CPU나 코어를 사용하여 여러 작업을 실제로 동시에 실행한다. 이는 과학 연산, 이미지 처리, 대규모 데이터 분석 등의 분야에서 특히 중요하다.

멀티 프로그래밍, 멀티태스킹, 멀티스레딩, 멀티프로세싱

멀티 프로그래밍 단점 : CPU 사용 시간이 길어지면 다른 프로세스는 계속해서 대기한다.

해결책 : 아주 짧은 시간(=quantum)만 CPU에서 실행되도록 하자

번갈아가면서 실행은 하지만 프로세스의 응답 시간을 최소화 시킨 것을 멀티 태스킹이라고 한다.

남아 있는 아쉬움

  • 하나의 프로세스가 동시에 여러 작업을 수행하지는 못함

  • 프로세스의 컨텍스트 스위칭은 무거운 작업임

  • 프로세스끼리 데이터 공유가 어려움

스레드

  • 프로세스는 한 개 이상의 스레드를 가질 수 있다.

  • CPU에서 실행되는 단위 (unit of execution)

  • 같은 프로세스의 스레드들끼리 컨텍스트 스위칭은 가볍다.

  • 스레드들은 자신들이 속한 프로세스의 메모리 영역을 공유한다.

  • Thread는 Program Counter, Stack Point 등을 비롯한 Thread 실행 환경 정보(Context 정보), 지역 데이터, Stack을 독립적으로 가지면서 Code, 전역 데이터, Heap을 다른 Thread와 공유함

  • Process Context Switching 보다 Thread Context Switching이 Overhead가 적고 다중 처리로 성능과 효율을 향상할 수 있다.

멀티 프로세싱

: 두개 이상의 코어에서 동작

컨텍스트 스위칭

컨텍스트 스위칭

: CPU 코어에서 실행 중이던 프로세스/스레드가 다른 프로세스/스레드로 교체되는 것

컨텍스트(context) : 프로세스/스레드의 상태, CPU , 메모리 등

컨텍스트 스위칭은 언제 발생하는가?

  • 주어진 time slice(quantum)을 다 사용했다

  • IO 작업을 해야 한다.

  • 다른 리소스를 기다려야 한다.

컨텍스트 스위칭은 누구에 의해 실행되는가?

OS 커널(kernel) : 각종 리소스를 관리/감독하는 역할

Process Context Switching ,Thread Context Switching

공통점 : 커널 모드에서 실행, CPU의 레지스터 상태를 교체

프로세스 컨텍스트 스위칭은 가상(Virtual) 메모리 주소 관련 처리를 추가로 수행

따라서 메모리 관련 영역인 MMU를 조정해주고 TLB를 비워줘야 한다.

컨텍스트 스위칭이 미치는 간접적인 영향

  • 캐시(cache) 오염(pollution)

사실 컨텍스트 스위칭은 순수한 오버헤드이다. 최대한 일어나지 않는 게 낫다.

IPC

critical section problem의 해결책이 되기 위한 조건
  1. Mutual Exclusion

  • 정의 : 둘 이상 프로세스/스레드가 동시에 공유 자원에 접근하지 못하게 하는 성질

  • 목적 : 데이터 레이스나 일관성 깨짐을 방지

  • 두 스레드가 같은 변수에 동시에 쓰기 못하게, 한 쪽이 임계 구역에 들어가면 다른 쪽은 나올 때까지 기다려야 한다.

  • 구현 방식 예:

    • 락 (mutex,spinlock)

    • 세마포어 (binary semaphore)

    • Peterson's algorithm

    • Test-and-set /Compare and Swap 같은 원자적 하드웨어 프리미티브

  1. Progress : 임계 구역에 들어갈 수 있는 상태라면 , 어떤 스레드가 들어갈지 결정하는 데에 불필요한 지연이 없어야 한다.

  1. Bounded Waiting

  • 정의: 어떤 스레드가 임계 구역을 요청한 이후, 다른 스레드들이 임계 구역에 진입하는 횟수가 어떤 상수 KKK 를 넘지 않으면 결국 그 스레드는 임계 구역에 들어갈 수 있다는 보장. 즉, 무한 대기(starvation) 가 발생하지 않도록 “기다리는 횟수에 상한”이 있다.

  • 의미: 공정성(fairness) 관련. 요청한 순서에 따라 너무 오래 밀려나지 않도록 보장한다.

예: 스레드 A가 진입 요청한 뒤, 다른 스레드들이 계속 들어가면 A는 계속 밀려날 수 있다. Bounded waiting이 있으면 “A가 요청한 이후 다른 스레드가 임계 구역에 진입할 수 있는 최대 횟수”가 고정된 값 이하라, 결국 A도 들어간다.

Spinlock, mutex, semaphore

race condition(경쟁 조건)

여러 프로세스/스레드가 동시에 같은 데이터를 조작할 때 타이밍이나 접근 순서에 따라 결과가 달라질 수 있는 상황

동기화(synchronization)

여러 프로세스/스레드를 동시에 실행해도 공유 데이터의 일관성을 유지하는 것

critical section(임계 영역)

공유 데이터의 일관성을 보장하기 위해 하나의 프로세스/스레드만 진입해서 실행(mutual exclusion) 가능한 영역

Spinlock 스핀락 방식

volatile int lock = 0;

void critical() {
    while(test_and_set(&lock) == 1);
    ...critical section
    lock = 0;
}

int TestAndSet(int* lockPtr) {
    int oldLock = *lockPtr;
    *lockPtr = 1;
    return oldLock;
}

TestAndSet을 통해 critical section에 하나의 스레드만 진입할 수 있다. TestAndSet은 CPU atomic 명령어 이기에 실행 중간에 간섭받거나 중단되지 않고, 같은 메모리 영역에 대해 동시에 실행되지 않는다.

mutex 뮤텍스

class Mutex {
    int value = 1;
    int guard = 0;
}

Mutex::lock() {
    while(test_and_set(&guard));
    if(value == 0) {
        .. 현재 스레드를 큐에 넣음;
        guard = 0; & go to sleep
    } else {
        value = 0;
        guard = 0;
    }
}

Mutex::unlock() {
    while(test_and_set(&guard));
    if(큐에 하나라도 대기중이라면) {
        그 중에 하나를 깨운다;
    } else {
        value = 1;
    }
    guard = 0;
}

mutex -> lock();
... critical section
mutex -> unlock();

value의 값이 1이 되어야 진입 가능.
value 자체도 공유 변수이기에 race condition이 발생할 수 있기에 guard라는 값을 취급한 후에야
value 값을 변경할 수 있다.

멀티 코어 환경이고 critical section에서의 작업이 컨텍스트 스위칭보다 더 빨리 끝난다면 스핀락이 뮤텍스보다 더 이점이 있다. (단일 코어면 스핀락이면 cpu 소모하는거니까)

semaphore(세마포)

signal mechanism을 가진, 하나 이상의 프로세스/스레드가 critical section에 접근 가능하도록 하는 장치

class Semaphore {
    int value = 1; //0,1,2,3
    int guard = 0;
}

Semaphore::wait() {
    while(test_and_set(&guard));
    if(value == 0) {
        ... 현재 스레드를 큐에 넣음
        guard = 0 & go to sleep
    } else {
        value -= 1;
        guard = 0;
    }
}

Semaphore::signal() {
    while (test_and_set(&guard));
    if(하나라도 대기중이라면)  {
        그 중에 하나를 꺠워서 준비시킨다.
    } else {
     value += 1;
 }
 guard = 0;
}

task 1 이 끝난 뒤 task 3 실행할 수 있음

  • p1이 먼저 signal() 호출 -> value = 1 -> task2 실행 후 wait -> value = 0 -> task 3

  • p2가 먼저 wait() 호출 -> value가 0이므로 스레드를 큐에 넣음 -> p1이 signal 호출 -> 잠자고 있던 p2 깨어나고 task3 실행

  • semaphore의 wait과 signal이 같은 프로세스에서 실행될 필요가 없다.

뮤텍스와 이진 세마포는 같은 것인가?

  • 뮤텍스는 락을 가진 자만 락을 해제 할 수 있지만 세마포는 그렇지 않다.

  • 뮤텍스는 priority inheritance 속성을 가진다.

  • 세마포는 그 속성이 없다.

p2의 우선 순위를 P1까지높여서 Lock을 가진 P2가 빨리 critical section을 빠져나올 수 있게 한다. 이는 락을 가진 자만 락을 해제할 수 있기 때문에 발생하는 현상이다.

상호 배제만 필요하다면 뮤텍스를

작업 간의 실행 순서 동기화가 필요하다면 세마포를 권장

Monitor(모니터)

비유 : 뮤텍스와 조건 변수를 결합하여, 공유 자원과 관련된 연산들을 하나의 객체로 캡슐화 한 것.

  • mutual exclusion을 보장

  • 조건에 따라 스레드가 대기(waiting) 상태로 전환 가능

모니터는 언제 사용되나?

  • 한번에 하나의 스레드만 실행되야 할 때

  • 여러 스레드와 협업이 필요할 때

모니터의 구성 요소

  • mutex

  • condition variables

cirical section에 진입하려면 mutex lock을 취득해야 함

mutex lock을 취득하지 못한 스레드는 큐에 들어간 후 대기(waiting) 상태로 전환

mutex lock을 쥔 스레드가 lock을 반환하면 락을 기다리며 큐에 대기 상태로 있던 스레드 중 하나가 실행

condition variable

  • waiting queue를 가짐

  • 조건이 충족되길 기다리는 스레드들이 대기 상태로 머무는 곳

  • 특정 조건이 만족될 때까지 스레드를 대기(wait) 시키거나 , 조건이 만족되었을 때 대기 중인 스레드를 깨우는 signal notify 매커니즘 제공

condition variable에서 주요 동작 (operation)

  • wait : thread가 자기 자신을 condition variable의 wating queue에 넣고 대기 상태로 전환

  • signal : waiting queue에서 대기중인 스레드 중 하나를 깨움

  • broadcast : waiting queue에서 대기중인 스레드 전부를 깨움

wait을 호출하게되면 m 락을 반환함. waiting queue에 들어가게 되고 cv 조건이 만족되면 waiting queue에 들어가 있는 작업들을 다른 곳에서 깨워줄 수 있다.

좋습니다. 이 내용을 더 깊이 있고 명확하게 정리해 드릴게요. Java에서의 동기화(synchronization), 모니터(monitor) 개념, 그리고 synchronized의 동작 방식에 대해 다음과 같이 이해하면 좋습니다.


✅ 1. 왜 동기화가 필요한가?

자바 객체는 상태(state) 값을 갖고, 이 값이 여러 스레드에 의해 동시에 접근 또는 변경될 수 있습니다. → 이로 인해 데이터 불일치, 예기치 못한 결과(경쟁 조건) 등이 발생할 수 있습니다. 그래서 자바는 이 문제를 해결하기 위해 객체 수준에서 "모니터(Monitor)" 라는 잠금(lock) 개념을 제공합니다.


✅ 2. Java 객체와 모니터(Monitor)

  • 모든 객체(Object)기본적으로 하나의 모니터(잠금)을 가진다.

  • 이 모니터를 사용해서 synchronized 블록은 임계 구역(Critical Section) 을 만들고, 이 구역에 하나의 스레드만 진입할 수 있도록 제한합니다.

✴ 예시

public synchronized void doSomething() {
    // 임계 구역 - 하나의 스레드만 접근 가능
}

위 메서드는 이 객체의 모니터를 획득한 스레드만 실행할 수 있습니다.


✅ 3. synchronized 키워드의 두 가지 형태

✔ 1. synchronized 메서드

public synchronized void methodA() { }
  • 인스턴스 메서드의 경우 → 해당 인스턴스(this)의 모니터를 lock합니다.

  • static 메서드의 경우 → 해당 클래스의 Class 객체의 모니터를 lock합니다.

✔ 2. synchronized(obj) 블록

synchronized (someObject) {
    // someObject의 모니터를 획득한 스레드만 실행 가능
}
  • 특정 객체의 모니터를 명시적으로 사용합니다.

  • finer-grained control (정밀한 제어)가 가능합니다.


✅ 4. Thread 클래스도 객체이기 때문에 모니터를 가짐

맞습니다. Thread 인스턴스도 하나의 객체이므로 모니터를 1개 기본적으로 가집니다.

즉, Thread 클래스 자체가 특별해서가 아니라, 자바에서 모든 객체가 모니터를 갖는 설계 구조에 따른 것입니다.


✅ 5. 스레드 안전(Thread Safety)을 확보하는 이유

다음과 같은 상황에서는 동기화가 반드시 필요합니다:

  • 여러 스레드가 동일 객체의 공유된 필드에 접근할 때

  • 특히, 쓰기(write) 연산이 있을 때는 경쟁 상태를 피해야 하므로 synchronized가 필수


✅ 정리 요약

개념
설명

객체의 모니터

모든 객체는 하나의 모니터(잠금)를 가지고 있음

synchronized

해당 블록/메서드의 객체 모니터를 lock함

동기화 목적

여러 스레드 간 데이터 충돌, 불일치를 방지하기 위함

synchronized(obj)

특정 객체에 대한 모니터를 명시적으로 획득


✅ 참고 예제

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

혹은

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }
}

두 방법 모두 count 필드를 thread-safe하게 만듭니다.

synchronized자바 객체가 가지고 있는 모니터(monitor) 를 기반으로 JVM 수준에서 구현된 동기화 메커니즘입니다.


✅ 다시 말해 정리하면:

🔹 synchronized"문법"

자바에서 제공하는 키워드이고, JVM이 이 키워드를 보면 자동으로 해당 객체의 모니터를 이용해서 락(lock)을 걸고 풀도록 동작합니다.

🔹 모니터는 "구현체"

모든 자바 객체가 가지고 있는 내부 락 구조, 즉 monitor lock이라는 구조로, synchronized는 이 모니터 락을 이용해서 임계 영역에 하나의 스레드만 접근할 수 있도록 보장합니다.


✅ 예시로 풀어보면

synchronized (someObject) {
    // someObject의 모니터를 획득 → 임계 구역
}

이 코드가 실행되면:

  1. someObjectmonitor lock을 현재 스레드가 시도해서 획득.

  2. 획득 성공 시 → 블록 진입.

  3. 블록을 빠져나갈 때 → 자동으로 락 해제.

  4. 다른 스레드가 기다리고 있었다면 → 다음 스레드가 monitor를 재획득.


✅ JVM 레벨에서 보면?

JVM 내부적으로 monitorentermonitorexit라는 바이트코드 명령어로 컴파일됩니다:

자바 코드
JVM 바이트코드

synchronized(obj) { ... }

monitorenter / monitorexit

즉, 정확히는 JVM이 monitor라는 구조체를 사용해서 락을 구현한 것입니다.


✅ 요약

요소
설명

synchronized

자바 문법 (JVM이 모니터를 사용하도록 지시함)

모니터(Monitor)

객체에 내장된 락 시스템. JVM이 관리하는 구조체

관계

synchronized → 해당 객체의 모니터를 이용해 동기화 구현


좋은 질문입니다! synchronizedObject.wait(), notify(), notifyAll()은 모두 Java의 모니터(Monitor) 를 기반으로 작동하는 기능들입니다. 서로 밀접한 관계가 있습니다. 아래에서 개념부터 JVM 동작까지 단계적으로 설명드릴게요.


✅ 1. Object.wait() / notify() / notifyAll() 개요

이 메서드들은 Object 클래스에 정의되어 있으며, 모든 자바 객체가 모니터를 갖고 있기 때문에 사용할 수 있습니다.

✴ 기능 요약

메서드
설명

wait()

현재 스레드가 락을 잠시 내놓고 대기(wait set에 들어감)

notify()

wait 중인 스레드 1개를 깨움 (하지만 락은 즉시 안 줌)

notifyAll()

wait 중인 모든 스레드를 깨움 (경쟁적으로 락을 획득하려 함)


✅ 2. 왜 synchronized 안에서만 사용 가능한가?

❗ 규칙

IllegalMonitorStateException

다음과 같이 쓰면 예외 발생합니다:

someObject.wait();  // ❌ synchronized 없이 호출 → 예외!

🔍 이유:

  • wait()/notify()는 해당 객체의 모니터에 접근하기 때문에,

  • 반드시 그 객체에 대한 synchronized 블록 안에서만 호출해야 합니다.

  • 즉, 모니터 락을 소유한 스레드만 wait/notify를 사용할 수 있음.


✅ 3. 내부 동작 관계 요약

🔹 synchronized → 모니터 획득

  • 블록에 진입한 스레드는 해당 객체의 모니터 락을 가지고 있음

🔹 wait()락 반납 + 대기

  • 현재 모니터 락을 반납하고, 해당 객체의 wait set에 들어감

  • 다른 스레드가 락을 획득할 수 있게 됨

🔹 notify() / notifyAll()스레드 깨움

  • 대기 중인 스레드를 깨우되, 즉시 실행되지는 않음

  • 깨어난 스레드는 락을 다시 획득할 수 있어야 실행 재개


✅ 4. 예제

public class SharedObject {
    private boolean ready = false;

    public synchronized void waitUntilReady() throws InterruptedException {
        while (!ready) {
            wait(); // 모니터 락을 반납하고 대기
        }
        System.out.println("Proceeding!");
    }

    public synchronized void markReady() {
        ready = true;
        notify(); // 하나의 스레드를 깨움 (락은 여전히 이 메서드 끝나기 전까지 소유)
    }
}

✅ 5. JVM 관점에서의 흐름

동작
JVM 명령어
설명

synchronized(obj)

monitorenter / monitorexit

모니터 락 획득 및 해제

obj.wait()

JVM에서 현재 스레드를 obj의 wait set으로 이동, 모니터 반납

obj.notify()

wait set에 있는 스레드 중 하나를 깨움 (스케줄링 대상에 올림)


✅ 6. 시각적으로 요약

          [Thread A]              [Thread B]

       synchronized(obj)       synchronized(obj)
       ┌────────────────┐      ┌────────────────┐
       │ obj.wait()     │◀─────┤ obj.notify()   │
       │ (락 반납 + 대기)│      │ (스레드 깨움)   │
       └────────────────┘      └────────────────┘

✅ 요약

개념
설명

wait()

현재 스레드가 락을 내려놓고 대기

notify()

대기 중인 스레드 1개를 깨움 (락은 나중에)

notifyAll()

대기 중인 모든 스레드를 깨움

synchronized

이 모든 메서드를 사용하기 위한 모니터 락의 획득 조건


Last updated