동시성 프로그래밍(Concurrency Programming)을 다루다 보면 Context Switching(문맥 교환)이라는 용어를 자주 접하게 된다. OS는 하나의 CPU 코어에서 여러 작업을 동시에 처리하는 것처럼 보이기 위해 실행 중인 작업의 상태를 저장하고 다른 작업으로 전환하는 과정을 반복하는데 이때 발생하는 핵심 메커니즘이 바로 Context Switching이다.
특히 현대 안드로이드 개발 환경에서는 빼놓을 수 없는 코루틴이 “경량 스레드”로 불릴 수 있는 이유 또한 전통적인 스레드의 Context Switching 비용을 어떻게 다루는지와 깊은 관련이 있다. 이 글에서는 Context Switching이 무엇인지 그리고 스레드와 코루틴의 전환 방식이 어떻게 다른지를 중심으로 살펴보려 한다.
프로세스
먼저 프로세스에 대한 기본 개념부터 알아보자. 프로그램을 실행하면 OS는 해당 실행 파일을 기반으로 하나의 프로세스를 생성하고 프로세스마다 독립적인 가상 메모리 공간을 할당한다. 커널 영역에는 이 프로세스를 관리하기 위한 프로세스 제어 블록(Process Control Block, PCB)이 생성되며 사용자 영역에는 실행 코드(Code), 전역 데이터(Data), 동적 메모리 영역(Heap), 호출 스택(Stack)으로 구성된 메모리 구조가 형성된다.

- Code 영역 : 실행할 프로그램의 기계어 명령어가 저장되는 영역으로 읽기 전용(read-only)이다.
- Data 영역 : 전역 변수와 정적 변수처럼 프로그램 전체에서 공유되는 데이터가 저장된다.
- Heap 영역 : 런타임 중 동적으로 할당되는 객체나 배열이 저장된다.
- Stack 영역 : 스택 트레이스(함수 호출 정보, 지역 변수, 매개변수, 반환 주소 등)가 저장된다.
이러한 메모리 구조는 각 프로세스마다 독립적인 가상 메모리 공간으로 할당된다. 즉, 서로 다른 프로세스는 코드와 데이터, 힙, 스택 영역을 서로 공유하지 않는다. 이 점이 이후 프로세스 문맥 교환 비용을 이해하는 핵심 포인트가 된다.
프로세스 제어 블록 (Process Control Block, PCB)
PCB는 OS가 특정 프로세스를 식별하기 위한 고유한 ID(PID)와 프로세스의 상태, CPU 스케줄링과 Context Switching을 수행하기 위해 필요한 모든 메타데이터를 담고 있는 자료구조이다.

OS는 이와 같은 사용자 영역의 컨텍스트와 커널 영역에 저장된 PCB 정보를 함께 관리함으로써 프로세스의 생명주기를 제어한다. 특히 프로세스 간 전환이 발생하는 Context Switching 과정에서 PCB는 이전 프로세스의 상태를 저장하고 다음 프로세스의 실행을 복원하는 역할을 수행한다.
Context Switching
메모리에 적재된 여러 프로세스들은 동시에 실행되는 것처럼 보이지만 실제로 단일 CPU 환경에서는 아주 짧은 시간 단위로 CPU를 번갈아 할당받아 실행된다. 이때 "프로세스가 실행된다"는 말은 곧 OS로부터 CPU 자원을 할당받아 명령어를 수행하고 있다는 의미이다.
OS는 공정한 CPU 사용과 시스템 응답성을 보장하기 위해 하나의 프로세스가 CPU를 독점하지 못하도록 타이머 인터럽트(timer interrupt) 를 사용한다. 타이머 인터럽트는 일정 시간이 지나면 강제로 발생하는 하드웨어 인터럽트로 현재 실행 중인 프로세스의 작업을 중단시키고 OS가 다시 CPU 제어권을 회수할 수 있도록 한다.
타이머 인터럽트가 발생하면 CPU는 커널 모드로 전환되고 OS는 현재 실행 중이던 프로세스의 실행 상태를 저장한 뒤 다음에 실행할 프로세스를 선택한다. 이때 저장되는 실행 상태에는 프로그램 카운터(PC), 레지스터 값, 스택 포인터 등 CPU 실행에 필요한 모든 정보가 포함되며 이 정보는 해당 프로세스의 PCB(Process Control Block)에 기록된다.
이처럼 하나의 프로세스에서 다른 프로세스로 CPU 실행 흐름을 전환하기 위해 현재 프로세스의 실행 문맥(context)을 저장하고 다음 프로세스의 실행 문맥을 복원하는 과정을 Context Switching(문맥 교환) 이라고 한다.
Process Context Switching vs Thread Context Switching
Context Switching에 대한 기본 개념을 살펴봤으니 Process Context Switching과 Thread Context Switching의 차이를 본격적으로 알아볼 차례다. 이를 이해하기 위해서는 먼저 프로세스 내부의 실행 단위인 스레드(Thread)가 어떤 구조를 가지는지 명확히 짚고 넘어갈 필요가 있다.
하나의 프로세스 내부에는 하나 이상의 스레드가 존재할 수 있으며 이 스레드들은 다음과 같은 특징을 가진다.
- 같은 프로세스에 속한 스레드들은 Code, Data, Heap 영역을 서로 공유한다.
- 각 스레드는 자신만의 Stack 영역과 레지스터 상태(PC, SP 등)를 가진다.

이 개념은 프로세스와 스레드의 Context Switching의 비용 차이를 이해하는 중요한 포인트다.
프로세스 간 Context Switching이 발생할 경우 서로 다른 가상 메모리 공간을 사용하므로 CPU 레지스터 상태뿐 아니라 메모리 맵(Address Space) 자체를 교체해야 한다. 이 과정에서 TLB(Translation Lookaside Buffer) 플러시 등 추가적인 비용이 발생한다.
메모리 맵(Address Space)
하나의 프로세스가 사용하는 가상 메모리 주소와 그 주소가 실제 물리 메모리의 어느 위치에 매핑되어 있는지를 나타내는 정보의 집합이다. 각 프로세스는 서로 독립적인 가상 주소 공간을 가지며 같은 가상 주소라 하더라도 프로세스가 다르면 다른 물리 메모리를 가르킨다. 이 가상 주소와 물리 주소의 매핑 정보는 페이지 테이블(Page Table)에 의해 관리된다.
TLB(Translation Lookaside Buffer)
CPU는 매번 메모리에 접근할 때마다 페이지 테이블을 조회해 논리 주소를 물리 주소로 변환해야 한다. 하지만 이 작업은 비용이 크기 때문에 최근에 사용된 주소 변환 결과를 캐시해 두는 TLB(Translation Lookaside Buffer)라는 하드웨어 캐시를 사용한다.
프로세스가 전환되면 새로운 프로세스는 전혀 다른 주소 공간을 사용하게 되므로 기존 TLB에 저장된 주소 변환 정보는 더 이상 유효하지 않다. 이 때문에 프로세스 Context Switching이 발생할 때는 TLB를 비우는(TLB Flush) 작업이 함께 수행되며 이 과정이 프로세스 전환 비용을 크게 만드는 원인 중 하나가 된다.
반면 스레드 간 Context Switching은 같은 프로세스 내부에서 이루어지기 때문에 공유 메모리 공간(Code, Data, Heap)과 페이지 테이블이 변하지 않는다. 때문에 메모리 맵이 유지되며 TLB 플러시 또한 발생하지 않는다.
이로 인해 스레드 Context Switching은 레지스터 상태와 스택 포인터 정도만 교체하면 되며 프로세스 Context Switching에 비해 훨씬 가볍고 빠르게 수행될 수 있다.
Coroutine Context Switching
지금까지 살펴본 프로세스와 스레드의 Context Switching은 모두 OS 커널이 개입하는 전환이었다. 타이머 인터럽트에 의해 실행이 강제로 중단되고 커널 모드로 전환된 뒤, PCB나 새로운 스레드 컨텍스트를 저장·복원하는 과정이 필요하다.
반면 코루틴(Coroutine) 은 전혀 다른 방식으로 동작한다. 코루틴의 전환은 커널이 아닌 사용자 공간(User Space) 에서 이루어지며 OS 입장에서는 스레드가 계속 실행 중인 것처럼 보인다. 즉, 코루틴 간의 전환은 Thread Context Switching이 아니라 하나의 스레드 내부에서 실행 흐름을 바꾸는 작업에 가깝다.
코루틴은 다음과 같은 특징을 가진다.
- OS가 관리하는 스케줄링 대상이 아니다.
- PCB나 TCB(Thread Control Block)를 교체하지 않는다
- 커널 모드 전환, 타이머 인터럽트, TLB Flush가 발생하지 않는다
이 때문에 코루틴의 Context Switching은 스레드 Context Switching에 비해 압도적으로 비용이 낮다.
협력적(Cooperative) 스케줄링 vs 선점형(Preemptive) 스케줄링
코루틴이 커널 개입 없이 전환될 수 있는 이유는 스케줄링 방식의 차이에 있다.
선점형 스케줄링 (Preemptive Scheduling)
프로세스와 스레드는 선점형 스케줄링을 따른다. 이는 OS가 현재 작업중인 CPU 자원을 강제로 빼앗아 다른 프로세스에 할당할 수 있는 스케줄링을 말한다. 앞서 설명한 타이머 인터럽트 기반 스케줄링은 모두 선점형 스케줄링의 대표적인 예다.
OS는 일정한 시간 간격으로 타이머 인터럽트를 발생시켜 현재 실행 중인 작업을 중단하고 스케줄러를 통해 다음 실행 대상을 선택한다. 이 과정이 매우 짧은 시간 단위로 반복되면서 싱글 코어 CPU 환경에서도 여러 작업이 마치 동시에 실행되는 것처럼 보이게 되며 이를 멀티 태스킹이라 한다.
선점형 스케줄링의 장점은 다음과 같다.
- 공정성(Fairness): 특정 작업이 CPU를 독점하지 못한다.
- 응답성(Responsiveness): UI 스레드가 멈추지 않고 시스템 전체가 굼떠지는 상황을 줄일 수 있다.
하지만 그 대가로 다음과 같은 단점이 있다.
- 각 작업은 자신이 언제 중단될지 미리 알 수 없다.
- 실행 대상이 바뀔 때마다 커널 개입과 컨텍스트 저장/복원이 발생한다.(Context Switching)
- 특히 프로세스 전환에서는 메모리 주소 공간 교체, TLB flush 같은 작업이 추가되면서 비용이 더 커진다.
비선점형/협력적 스케줄링 (Non-Preemtive / Cooperative Scheduling)
프로세스가 CPU를 사용하고 있을 때 프로세스가 종료되거나 스스로 대기 상태에 접어들기 전까지 실행 중인 작업을 외부에서 강제로 중단시키지 않고 작업 스스로가 CPU 제어권을 반납할 때까지 기다리는 방식이다.
이 방식에서는 OS가 타이머 인터럽트를 통해 강제로 CPU를 회수하지 않으며 작업이 명시적으로 CPU를 양보할 때까지 실행을 계속한다. 이러한 특성 때문에 비선점형 스케줄링은 협력적(Cooperative) 스케줄링이라고도 불린다.
코루틴은 이러한 협력적 스케줄링 방식을 기반으로 동작한다. 다만 단순히 “알아서 CPU 사용 권한을 반납한다” 수준에 머무르지 않고 언어 수준에서 안전 장치를 함께 제공한다는 점에서 전통적인 비선점형 모델과 차이가 있다.
코루틴에서 실행 흐름이 전환될 수 있는 시점은 suspend 키워드로 정의된 중단 지점으로 제한된다. 이 지점에 도달했을 때 코루틴은 현재 실행 상태를 저장하고 스레드를 점유한 채 대기하지 않고 즉시 반환한다. 이후 동일한 스레드에서는 다른 코루틴이 실행되며 중단된 코루틴은 필요할 때 다시 이어서 실행된다.
이 과정은 모두 사용자 공간(User Space)에서 이루어진다. OS 입장에서는 하나의 스레드가 계속 실행 중인 것처럼 보이며 타이머 인터럽트, 커널 모드 전환, PCB, TCB 교체와 같은 OS 레벨의 Context Switching은 발생하지 않는다. 그 결과 코루틴 간 전환 비용은 스레드 Context Switching보다 훨씬 낮다.
Continuation
코루틴이 협력적 스케줄링을 안전하게 구현할 수 있는 핵심에는 Continuation이라는 개념이 있다. Continuation은 코루틴의 실행 환경을 담은 CoroutineContext와 중단 지점 이후의 실행 흐름을 재개하기 위한 모든 정보를 담고 있는 객체다.

코루틴이 suspend 지점에 도달하면 현재 실행 흐름은 Continuation 객체로 변환되어 힙(Heap)에 저장된다.Continuation에는 다음과 같은 정보가 포함된다.
- 다음에 실행할 코드 위치 (Program Counter)
- 중단 지점 이후에 필요한 로컬 변수 상태
- 다음 상태로 전이하기 위한 상태 머신 정보
즉, 코루틴은 실행을 그대로 중단하는 것이 아니라 실행 흐름 자체를 객체로 분리하여 보관하는 방식으로 동작한다.
마치며
최근 취업 준비를 하며 가장 크게 느끼고 있는 것은 새로운 기술을 빠르게 따라가는 것보다 이미 사용하고 있는 기술을 얼마나 깊이 이해하고 있는지가 더 중요하다는 점이었다. 코루틴 경량 스레드라고 불린다면 이를 문장 그대로 받아들이는데서 멈추지 않고 왜 그렇게 불리는지, 스레드와는 어떤 차이를 가지는지를 OS 관점에서 되짚어보는 과정 자체가 큰 의미가 있었다.
이번 글에서 기술한 내용 외에도 코루틴의 동작 방식을 이해하기 위해선 더 많은 내용을 학습해야 한다. 다만 아직은 그 부분을 이해하기엔 나에게 조금 어려운 내용들이기에 시간이 필요한 것 같다.
'CS' 카테고리의 다른 글
| 자바 프로그램 실행 과정 및 기본 구조 (1) | 2024.01.02 |
|---|---|
| 프로세스와 스레드 (2) | 2023.12.31 |
