티스토리 뷰

기본기는 가볍게 정리해본다.

프로세스

  • 프로세스는 운영체제에 의해 실행되는 포로그램이다.
  • code, data, heap, stack 메모리 공간을 갖는다.
  • stack은 지역변수, heap은 메모리 등을 사용하게 되는 동적 할당의 영역이다.
  • code, data는 정적 할당의 영역이다.

스레드

  • 스레드는 프로세스의 작업을 수행하는 최소 실행 단위이다.
  • 스레드는 프로세스의 code, data, heap은 공유자원으로 사용한다.
  • 스레드는 개별 stack을 갖는다.

CPU

  • 스레드는 운영체제 스케쥴러에 의해 관리되는 CPU의 최소 실행 단위이다.
  • 스레드가 CPU를 선점하게된다.
  • CPU를 스레드가 선점할 때 문맥(레지스터, 캐시 등이 교체되는) 교환이 발생한다. (=컨텍스트 스위칭)

동시성 / 병렬성 개념을 정리해보자.

동시성

  • 동시성은 CPU가 한꺼번에 여러개의 작업을 매우 빠르게 번갈아가면서 하는것이다.
  • 타임 슬라이스 기법으로 여러 작업을 매우 빠르게 교체해가며 처리한다.
    우리는 이것을 통해 여러 프로그램을 동시에 사용하고 있다는 착각에 빠진다.
  • 만약 해야할 작업이 2개인데 CPU 코어는 1개인 경우 동시성이 발생한다.
    즉, 작업이 CPU 수보다 많은 경우에 분할(Time Slicing)을 통한 동시성이 발생한다.
  • 동시성이 없다면, 작업은 순차처리하게 된다.

병렬성

  • 복수의 CPU가 동시에 많은 일을 수행하는 것이다.
  • 단일 코어 CPU에는 병렬성이 구현될 수 없다.
  • 병렬성은 해야할 작업이 CPU 코어 수 보다 작거나 같을 때 가장 효율적이다.

병렬성 & 동시성 조합

ThreadPoolExecutor

CPU 1 ─── [Task] ─ [Task] ─ [Task] ──── [Task] ─────────→ Thread 1
      ─── [Task] ─ [Task] ─ [Task] ─ [Task] ────────────→ Thread 2

CPU 2 ─── [Task] ─ [Task] ─ [Task] ──────── [Task] ─────→ Thread 3
      ─── [Task] ─ [Task] ─ [Task] ──── [Task] ─────────→ Thread 4
  • 스레드 풀을 만들어서 태스크를 처리하는 기법.
  • 병렬성으로 처리 성능을 극대화 하고 동시성으로 CPU 자원을 효율적으로 운용한다.
  • 작업간 의존이 없는 경우에 적합하다.
  • 고정 스레드 풀에서 처리되고 I/O 작업에서 주로 사용된다.

ForkJoinPool

CPU 1 ─── [Task] ─→ [Task] ─┬───────────────────────────────→ Thread 1
                            │
CPU 2 ──────────────────────┼─→ [Task] ─────────────────────→ Thread 2
                            │
CPU 3 ──────────────────────┴─→ [Task] ─┼─→ [Task] ─────────→ Thread 3
                                        │
CPU 4 ──────────────────────────────────┴─→ [Task] ─────────→ Thread 4
  • 하나의 태스크를 서브 태스크로 분할하여 병렬 처리한다.
  • 분할 정복 알고리즘에 최적화 되어있다.
  • Stream API의 parallel()에서 사용됨. (현재 사용 가능한 코어수에 의존)
  • 재귀적 작업에 적합하다.
  • Virtual Thread의 처리 방식이 ForkJoinPool 방식이다.

커널 모드에 대해 알아보자. 사용자 모드 / 커널 모드

### Application Layer
┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│ Application │  │ Application │  │ Application │
└──────┬──────┘  └──────┬──────┘  └──────┬──────┘
       │                │                │
       └────────────────┼────────────────┘
                        │
                        ↓
┌───────────────────────────────────────────────────┐
│              Operating System                     │
│  ┌─────────────────────────────────────────────┐  │
│  │              Kernel                         │  │
│  └─────────────────────────────────────────────┘  │
└───────────────────────┬───────────────────────────┘
                        │
                        ↓
┌───────────────────────────────────────────────────┐
│              Hardware                             │
│        (cpu, memory, I/O 장치)                     │
└───────────────────────────────────────────────────┘
  • 운영체제는 컴퓨터 자원을 효율적으로 관리하는 소프트웨어이고, 이를 위해 여러 기능을 갖고있다.
    이중 핵심 기능을 담당하는 부분이 커널(Kernel)이다.
  • 운영체제 위에서 동작하는 사용자 프로그램들을 편하고 효율적으로 사용할 수 있게
    하드웨어와 소프트웨어(애플리케이션) 간 중재자 역할을 하게된다.
  • 결국 사용자 애플레케이션은 자원을 독점하려고 하기 때문에 이를 중재하기 위한 강력한 중재자인
    운영체지가 존재하고 이를 위한 주요 기능으로 커널이 존재하는 것.
  • 사용자 모드에서 I/O 영역을 사용하기 위해 커널 모드에 들어가야 하고 이를 위해서는 시스템 콜을 통해서만
    커널 모드에 진입할 수 있다. 커널모드에 진입하면 그때 하드웨어 장치에 직접 제어가 가능해진다.

커널 수준 스레드와 사용자 수준 스레드를 알아보자.

사용자 수준 스레드

  • 사용자 애플리케이션에서 관리하는 스레드이다.
  • 사용자 영역으로 스레드 라이브러리들에 의해 스레드의 생성, 종료, 메시지 전달, 스케쥴링 보관 등 모든것을 관리한다.
  • 커널은 사용자 수준 스레드에 대해 알지 못하고 단일 스레드 프로세스로 인식해서 관리한다.

커널 수준 스레드

  • OS에서 관리하는 스레드이다.
  • 커널이 스레드와 관련된 모든 작업을 관리한다. PCB, TCB 관리 등
  • CPU는 커널에 의해 생성된 스레드의 실행만을 담당한다.

스레드 매핑 모델

  • CPU는 OS Sccheduler가 예약하는 커널 스레드만 할당받아서 실행시키므로 사용자 수준 스레드와 커널 수준 스레드간 매핑은 필수적이다.
  • 3가지 모델이 있다. 다대일 매핑 / 일대일 매핑 / 다대다 매핑

다대일 매핑
Many 사용자 수준 스레드 to One 커널 수준 스레드

  • 다수의 사용자 수준 스레드가 커널 수준 스레드 하나에 매핑된다.
  • 커널의 개입 없이 사용자 수준 스레드 끼리 스위칭이 발생하므로 오버헤드가 적다.
  • 개별 스레드 단위가 아닌 단일 스레드의 프로세스 단위로 프로세서를 할당하므로 멀티코어를 이용한 병렬처리를 할 수 없다.
  • 한 스레드가 Block I/O가 발생하면 모든 스레드들이 Block된다. 프로세스 자체를 블록하기 때문.

1번 프로세스 스레드 O, 스레드 O, 스레드 O <-> 커널 스레드 O <-> CPU Core

  • 프로세스 내부에서 우선순위를 통해 커널 스레드를 점유하는 것.

 

일대일 매핑
One 사용자 수준 스레드 to One 커널 수준 스레드

  • 사용자 수준 스레드 하나 당 커널 수준 스레드 하나가 매핑된다.
  • 커널이 전체 프로세스와 스레드 정보를 유지해야하므로 컨텍스트 스위칭 시 커널 모드로 전환해서 스케쥴링이 필요하다.
  • 자원의 한정으로 무한정 스레드를 만들순 없다.
  • 스레드 단위로 CPU를 점유하므로 멀티코어를 활용한 병렬 처리가 가능하다.
  • 스레드 하나가 IO Blocked 되더라도 다른 스레드를 사용할 수 있다. 따라서 동시성 처리가 가능하다.
  • 자바의 Native Thread가 여기에 해당한다. Platform Thread라고 함.

1번 프로세스 스레드 O, 스레드 O, 스레드 O <-> 커널 스레드 O, 스레드 O, 스레드 O <-> CPU Core, Core, Core

 

다대다 매핑
Many 사용자 수준 스레드 to Many 커널 수준 스레드

  • 여러개의 사용자 수준 스레드를 같거나 더 적은 수의 커널 수준 스레드에 매핑한다.
  • 사용자는 필요한 만큼 (커널 수준 스레드 한계에 구애받지 않고) 사용자 수준 스레드를 생성할 수 있고, 커널 수준 스레드가 멀티 프로세서에서 병렬로 수행될 수 있다.
  • 병렬성, 동시성 처리가 가능하다.
  • Java에서는 Virtual Thread, Kotlin에서는 Coroutine 으로 다대다 스레드 매핑 모델을 구현하고 있다.

1번 프로세스 스레드 O, 스레드 O, 스레드 O, 스레드 O, ... <-> 커널 스레드 O, 스레드 O, 스레드 O <-> CPU Core, Core, Core

병렬 수행 코드 예시

코어 수와 동일한 스레드 병렬 수행

fun `코어개수와 동일한 병렬수행`() {
    val core = Runtime.getRuntime().availableProcessors()

    val data = ArrayList<Int>()
    for (i in 1..core) {
        data.add(i)
    }

    val start: Long = System.currentTimeMillis()
    val sum = data
        .parallelStream()
        .mapToLong {
            try {
                Thread.sleep(500)
            } catch (_: Exception) {
                throw RuntimeException()
            }
            (it * it).toLong()
        }.sum()
    val end: Long = System.currentTimeMillis()

    println("코어개수와 동일한 병렬수행: sum: $sum, duration: ${end - start}ms")
}

  • sleep 시간 500ms와 유사한 시간이 소요되었다.
  • 스레드수와 코어수가 동일하므로 플랫폼 스레드가 생성되어서 각각 코어에 선점되어 처리가 된 것이다.

코어 수 보다 2배 많은 스레드 병렬 수행

fun `코어개수 두배의 병렬 수행`() {
    val core = Runtime.getRuntime().availableProcessors()

    val data = ArrayList<Int>()
    for (i in 1..core * 2) {
        data.add(i)
    }

    val start: Long = System.currentTimeMillis()
    val sum = data
        .parallelStream()
        .mapToLong {
            try {
                Thread.sleep(500)
            } catch (_: Exception) {
                throw RuntimeException()
            }
            (it * it).toLong()
        }.sum()
    val end: Long = System.currentTimeMillis()

    println("코어개수 2배의 병렬수행: sum: $sum, duration: ${end - start}ms")
}

  • sleep 시간 500ms의 2배인 1,000ms와 유사한 시간이 소요되었다.
  • 스레드 수가 코어 수의 2배이므로 한차례 선점 후 sleep(500)을 하고, 다시 다음 스레드들이 선점 후 sleep을 하게된 것이다.

코어 수 보다 1개 많은 스레드 병렬 수행

fun `코어개수 보다 한 개 많은 병렬 수행`() {
    val core = Runtime.getRuntime().availableProcessors()

    val data = ArrayList<Int>()
    for (i in 1..core + 1) {
        data.add(i)
    }

    val start: Long = System.currentTimeMillis()
    val sum = data
        .parallelStream()
        .mapToLong {
            try {
                Thread.sleep(500)
            } catch (_: Exception) {
                throw RuntimeException()
            }
            (it * it).toLong()
        }.sum()
    val end: Long = System.currentTimeMillis()

    println("코어개수 보다 한 개 많은 병렬 수행: sum: $sum, duration: ${end - start}ms")
}

  • sleep 시간 500ms의 2배인 1,000ms와 유사한 시간이 소요되었다.
  • 스레드 수가 코어 수보다 1개 더 많으므로 전체 스레드 - 1개가 각 코어에 전부 선점되었고, 이후 남은 1개 스레드가 코어에 선점되어 처리가 된것이다.

이처럼 코어 수에 맞게 스레드를 병렬 수행하는것이 가장 효율적이고, 코어가 N개일 때 N개의 작업을 각 스레드에 할당하여 병렬 처리하는 소요 시간은, 1개의 작업을 단독으로 처리할 때의 시간과 거의 동일함을 확인할 수 있다.

반응형
Comments
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday