[CS] 컨텍스트 스위칭

yb__char·2024년 11월 22일
0
post-thumbnail

이번엔 컨텍스트 스위칭에 대해서 이야기해보려합니다.
이전 동시성 프로그래밍에 관련한 키워드에 대해 이야기했었는데, 내용이 상당히 길었죠?

저도 좀 쓰면서 오반데....했습니다..🙏
이번에는 이후 코루틴에 대해 이야기하기 전에 알아야 할 선수지식들에 대해서 포스팅을 해보려합니다.

  1. 컨텍스트 스위칭
  2. Atomic (CAS), syncronized (lock), volatile
  3. Tomcat 네트워크 요청을 받아서, 스레드를 할당받고, 이게 스프링까지 넘어와서 어떤 식으로 스레드가 처리되는지?

위 항목들에 대해 다뤄야 추후 얘기할 코루틴과 Spring Webflux에 대해 조금이나마 도움이 될 듯하여 포스팅을 해볼까합니다.
(위 내용을 공부해도 Webflux는 어려운 내용입니다..)

컨텍스트 스위칭 (하드웨어적 관점)

우선 운영체제 영역에서 공부를 좀 해보셨다면 멀티 프로세싱/멀티 스레딩에 대해 공부를 좀 해보셨을 겁니다. 저는 학점을 위한 공부를 했어서 다시 상기시킬겸 공부를 해봤는데 미리미리 좀 할걸 그랬습니다..😅
뒷 내용 컨텍스트 스위칭에 대해 이야기하기 전 잠깐 알아보겠습니다.

멀티 프로세싱 및 멀티 스레딩

멀티 프로세싱
멀티 프로세스는 여러 개의 독립적인 CPU를 사용해 여러 프로세스를 병렬적으로 수행하는 것입니다.
이는 각 프로세스가 서로 영향을 주지 않으므로 안정성이 높지만, 프로세스 간 통신(IPC)을 위한 추가적인 오버헤드가 발생합니다.

멀티 프로세스의 장점은 위와 같이 각 프로세스가 독립된 메모리 공간을 가지기 때문에 하나의 프로세스가 실패해도 다른 프로세스에 영향을 주지 않는다는 점입니다.

단점으로는 프로세스 간 통신 비용이 높고, 메모리 사용량이 많다는 점을 들 수 있습니다. 각 프로세스가 독립된 메모리 공간을 가지므로, 같은 데이터를 여러 프로세스에서 사용할 경우 중복 저장되어 메모리 사용량이 증가할 수도 있습니다.

예를 들어, 웹 서버와 데이터베이스 서버를 별도의 프로세스로 실행하여, 하나의 서버에 문제가 발생해도 다른 서버는 정상적으로 작동할 수 있습니다.

멀티 스레딩
멀티 스레드는 하나의 프로세스 내에서 여러 스레드가 메모리를 공유하며 동시에 실행되는 방식입니다. 이는 프로세스 생성 비용보다 스레드 생성 비용이 훨씬 낮으며, 스레드 간 데이터 공유가 용이합니다.

멀티스레딩은 CPU의 사용률을 극대화하고, I/O 작업이 블로킹되는 동안 다른 스레드가 CPU를 사용할 수 있게 하기 때문입니다. 또한, 멀티스레딩은 프로그램의 구조를 단순화할 수 있으며, 사용자 인터페이스와 같은 비동기 작업을 용이하게 합니다. 이처럼 컴퓨터의 여러 코어 자원을 효율적으로 활용할 수 있어 대규모 연산이나 I/O 작업에서 큰 장점을 가질 수 있습니다

또 다른 장점으로는 메모리 공유로 인한 효율성입니다. 왜냐하면 스레드 간 데이터를 공유할 수 있으므로, 데이터 복사 비용이 줄어들고, 통신 비용이 낮아집니다. 각 요청을 별도의 스레드에서 처리하여 처리 속도를 높일 수 있습니다.

단점으로는 스레드 간의 동기화 문제가 발생할 수 있다는 점입니다. 공유된 메모리에 여러 스레드가 동시에 접근할 경우, 한없이 기다리는 데드락이나 레이스 컨디션에 빠지게됩니다. 이에 대한 적절한 동기화 기법으로 데이터의 일관성을 유지하기 위한 추가적인 작업이 필요합니다. 그 외 디버깅과 오버헤드에 단점이 존재합니다.


간단히 멀티 프로세싱/멀티 스레딩 이야기를 해보았습니다.
다음 본격적인 컨텍스트 스위칭에 대해 알아보겠습니다.

컨텍스트 스위칭 이해와 비용

CPU가 어떤 프로세스를 실행하고 있는 상태에서 인터럽트에 의해 다음 우선 순위를 가진 프로세스가 실행되어야 할 때 기존의 프로세스 정보들은 PCB에 저장하고 다음 프로세스의 정보를 PCB에서 가져와 교체하는 작업을 컨텍스트 스위칭이라 합니다. 이러한 컨텍스트 스위칭을 통해 우리는 멀티 프로세싱, 멀티 스레딩 운영이 가능합니다.

컨텍스트 스위칭 발생 시점

  • 주어진 time slice(=quantum)를 다 사용했을 때 (멀티태스킹 시스템),
  • I/O 작업을 해야할 때
  • 다른 리소스를 기다려야 할 때 (선점/비선점)
  • interrupt가 걸렸을 때

컨텍스트 스위칭 동작 과정

  1. 현재 작업의 상태 저장: CPU가 현재 작업(프로세스 또는 스레드)을 중단하기 전에, 그 작업의 모든 상태(프로그램 카운터, 레지스터 값 등)를 저장합니다. 이 정보는 보통 PCB 또는 TCB라는 메모리 구조에 저장됩니다.
  2. 다음 작업의 상태 복원: CPU는 대기 중인 다른 작업의 상태를 PCB나 TCB에서 불러와 복원합니다. 이때 CPU는 이전 작업이 어디에서 중단되었는지, 어떤 값을 가지고 있었는지를 알 수 있습니다.
  3. 새로운 작업 실행: CPU는 복원된 상태에서 다음 작업을 이어서 실행합니다. 이 작업이 끝나거나 일정 시간 동안 실행된 후, 다시 다른 작업으로 전환됩니다.

컨텍스트 스위칭 비용
컨텍스트 스위칭에는 오버헤드(Overhead)가 발생합니다. 즉, CPU가 작업을 전환할 때 상태를 저장하고 복원하는 과정은 일정한 시간이 소요되며, 이는 CPU 자원을 소모하게 됩니다. 컨텍스트 스위칭이 빈번하게 일어나면 CPU가 실제 작업을 처리하는 시간보다 오히려 스위칭에 소요되는 시간이 많아질 수 있습니다.

비용이 발생하는 이유는?

  1. 상태 저장과 복원: 현재 작업의 모든 상태를 저장하고 다음 작업의 상태를 복원하는 데 시간이 소요됩니다.
  2. 캐시 미스(Cache Miss): 새로운 작업으로 전환할 때, CPU 캐시가 새로운 작업에 맞게 다시 로드되어야 할 수 있습니다. 이는 캐시 미스를 발생시켜 성능 저하를 유발할 수 있습니다.
  3. TLB 플러시(TLB Flush): 컨텍스트 스위칭 시에 페이지 테이블이 새롭게 설정되어야 하므로, TLB(Translation Lookaside Buffer)라는 캐시 메모리가 무효화될 수 있습니다. (스레드가 아닌 프로세스 한정)

컨텍스트 스위칭 비용 절감 방법

  • 스케줄링: 특정 스레드를 특정 코어에 고정시켜 캐시의 재사용률을 높이는 기법으로, 하드웨어적으로 캐시 및 TLB 히트율을 높일 수 있습니다.

  • 스레드 풀(Thread Pool) 사용: 스레드 수 조정 및 효율적 관리로 새로운 스레드를 계속 생성하는 대신, 일정한 수의 스레드를 미리 만들어서 재사용하면 스위칭 빈도를 줄일 수 있습니다.

  • 락(lock) 사용 최소화: 불필요한 락 사용을 줄여 스레드 간 경쟁을 줄이고, 컨텍스트 스위칭 빈도를 줄일 수 있습니다.

  • 비동기 프로그래밍: I/O 작업을 기다리는 동안 CPU가 빈번한 컨텍스트 스위칭을 발생시키지 않도록 비동기 방식으로 작업을 처리합니다.

  • 코루틴 활용: 스레드 단위의 동시성 대신 경량화된 코루틴을 활용하면, 스레드 수준에서 발생하는 컨텍스트 스위칭을 줄일 수 있습니다.


Kotlin 코루틴을 활용한 컨텍스트 스위칭 비용 절감

Kotlin의 코루틴은 스레드의 물리적 전환 없이 논리적인 작업 단위 전환을 지원하여 하드웨어적 관점에서도 효율적입니다.

  • 경량화된 실행: 코루틴은 스레드와 달리 가벼운 작업 단위로, 메모리와 CPU 레지스터 값을 저장 및 복원하는 비용을 줄일 수 있습니다.
  • 스레드 간 전환 없이 작업 수행: 코루틴은 단일 스레드에서 다수의 작업을 효율적으로 처리하므로, 캐시 및 TLB 플러시와 같은 하드웨어적 비용을 감소시킵니다.
  • 비동기적 작업 관리: 코루틴은 동시성 작업을 논블로킹 방식으로 관리하므로, 블로킹으로 인한 스레드 스케줄링 및 컨텍스트 스위칭을 줄이는 효과가 있습니다.

Kotlin 코루틴 예제

아래는 코루틴을 사용하여 여러 작업을 효율적으로 실행하는 예제입니다. 이 예제는 두 개의 작업을 동시에 실행하며, 하드웨어 레벨의 스레드 전환 없이 동작하는 것을 보여줍니다.

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("시작 - ${Thread.currentThread().name}")

    // launch를 통해 코루틴을 시작합니다.
    val job1 = launch {
        task("Task1", 1000)
    }

    val job2 = launch {
        task("Task2", 1500)
    }

    // 모든 코루틴이 끝날 때까지 기다립니다.
    job1.join()
    job2.join()

    println("완료 - ${Thread.currentThread().name}")
}

suspend fun task(name: String, timeMillis: Long) {
    println("$name 시작 - ${Thread.currentThread().name}")
    delay(timeMillis) // 비동기적으로 지연시킵니다.
    println("$name 완료 - ${Thread.currentThread().name}")
}

이 코드에서는 runBlocking을 통해 메인 스레드에서 코루틴을 실행하고, launchdelay를 통해 하드웨어적인 스레드 전환 없이 두 개의 작업을 비동기적으로 수행합니다. delay를 사용하는 동안 다른 코루틴이 실행될 수 있어 스레드 전환 없이 비동기 처리가 가능합니다.


하드웨어 관점에서 본 코루틴의 장점

  • 캐시 효율성: 단일 스레드 내에서 여러 코루틴이 작동하므로, 캐시 데이터를 유지하며 작업을 처리할 수 있습니다.
  • 낮은 메모리 및 CPU 리소스 소모: 레지스터나 메모리 복원이 필요하지 않으므로, 메모리 접근 횟수가 줄어들고, CPU 자원을 절약할 수 있습니다.

Kotlin의 코루틴을 활용하여 스레드 단위의 전환 비용을 절감하고, 하드웨어 자원을 보다 효율적으로 사용할 수 있습니다. 이를 통해 고성능이 요구되는 애플리케이션에서도 보다 효율적인 동시성 처리를 기대할 수 있습니다.


결국 기승전 코루틴으로 끝나는 거 같지만, 코루틴은 경량 스레드로 컨텍스트 스위칭이 이뤄지기에 조금 다뤄봤습니다.

다음에는 Atomic (CAS), syncronized (lock), volatile에 대해 다뤄보겠습니다:)

Github Repository

profile
안녕하세요 백엔드 개발자 차윤범입니다 :)

0개의 댓글