동시성 프로그래밍을 다루다 보면 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는 이전 프로세스의 상태를 저장하고 다음 프로세스의 실행을 복원하는 역할을 수행한다.
스레드
앞서 살펴본 것처럼 프로세스는 독립적인 메모리 공간을 가지며 이는 안정성과 격리성을 제공하지만 동시에 한계도 존재한다. 만약 하나의 프로그램 내에서 여러 작업을 동시에 처리하고 싶다면 어떻게 해야 할까?
예를 들어 웹 브라우저에서 파일을 다운로드하면서 동시에 다른 탭에서 웹 페이지를 렌더링 하는 경우를 생각해 보자. 이러한 요구를 프로세스만으로 해결하려면 여러 프로세스를 생성해야 하는데 이는 다음과 같은 문제를 야기한다.
- 각 프로세스마다 독립적인 Code, Data, Heap, Stack 영역이 필요하므로 메모리 사용량이 크게 증가한다.
- 프로세스 간 데이터를 공유하려면 IPC(Inter-Process Communication) 같은 별도의 메커니즘이 필요하며 이는 복잡하고 느리다.
- 프로세스 간 전환 시 많은 비용이 수반된다.
이러한 문제를 해결하기 위해 등장한 것이 바로 스레드다. 스레드는 프로세스 내부의 실행 단위로 하나의 프로세스는 하나 이상의 스레드를 포함할 수 있으며 스레드의 핵심 특징은 다음과 같다
- 공유 영역: 같은 프로세스에 속한 스레드들은 Code, Data, Heap 영역을 공유한다.
- 독립 영역: 각 스레드는 자신만의 Stack 영역과 레지스터 상태(PC, SP 등)를 독립적으로 가진다.
이러한 구조 덕분에 스레드는 프로세스가 제공하는 격리성을 일부 포기하는 대신 공유 메모리를 통해 메모리 사용량이 줄어들고 데이터를 공유할 수 있다. 다만 메모리를 공유한다는 것은 동시성 문제(race condition, deadlock 등)를 유발할 수 있음을 유의해야 한다.
스레드 제어 블록 (Thread Control Block, TCB)

프로세스에 PCB가 있듯이, 스레드에도 스레드를 관리하기 위한 메타데이터를 담은 TCB(Thread Control Block)가 존재한다. TCB에는 스레드 ID, 스레드 상태, 레지스터 값, 스택 포인터 등 스레드 고유의 실행 정보가 저장된다. 다만 Code, Data, Heap 영역에 대한 정보는 프로세스의 PCB에 이미 존재하므로 TCB에는 포함되지 않는다.
Context Switching
메모리에 적재된 프로세스는 “실행 준비” 상태일 뿐이며 실제 실행되려면 반드시 CPU 사용 권한을 할당받아야 한다. 만약 하나의 프로세스가 CPU의 사용권한을 독점하면 다른 프로세스들은 실행될 수 없어 시스템 전체의 응답성이 저하된다.
때문에 OS는 공정한 CPU 사용권한의 분배와 시스템 응답성을 보장하기 위해 하나의 프로세스가 CPU를 독점하지 못하도록 타이머 인터럽트(timer interrupt)를 사용한다.
💡 타이머 인터럽트란?
일정 시간이 지나면 강제로 발생하는 하드웨어 인터럽트로 현재 실행 중인 프로세스의 작업을 중단시키고 OS가 다시 CPU 제어권을 회수할 수 있도록 한다.
타이머 인터럽트가 발생하면 다음의 과정을 거친다.
- CPU 커널 모드 전환
- OS는 현재 실행 중이던 프로세스의 실행 상태를 PCB에 저장한 뒤 다음에 실행할 프로세스를 선택
이때 저장되는 실행 상태에는 프로그램 카운터(PC), 레지스터 값, 스택 포인터 등 CPU 실행에 필요한 모든 정보가 포함되며 이 정보는 해당 프로세스의 PCB(Process Control Block)에 기록된다. PCB는 커널 공간에 위치하므로 이러한 작업은 반드시 커널 모드에서만 수행될 수 있다.

이처럼 하나의 프로세스에서 다른 프로세스로 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이 발생할 경우 서로 다른 가상 메모리 공간을 사용하므로 커널 모드로 전환되어 PCB에 프로세스의 상태를 저장하고 페이지 테이블을 교체한다.
이 과정에서 TLB(Translation Lookaside Buffer) 플러시와 같은 추가적인 비용이 발생한다.
페이지 테이블
하나의 프로세스가 사용하는 CPU상의 가상 메모리 주소와 그 주소가 실제 물리 메모리의 어느 위치에 매핑되어 있는지를 나타내는 정보의 집합이다. 각 프로세스는 서로 독립적인 가상 주소 공간을 가지며 같은 가상 주소라 하더라도 프로세스가 다르면 다른 물리 메모리를 가리킨다. 이 가상 주소와 물리 주소의 매핑 정보를 저장하는 것이 페이지 테이블이라 한다.

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 커널이 개입하는 전환이었다. 타이머 인터럽트에 의해 작업이 강제로 중단되고 커널 모드로 전환된 뒤 새로운 스레드 컨텍스트를 저장·복원하는 과정이 필요하다.
반면 코루틴(Coroutine) 은 전혀 다른 방식으로 동작한다. 코루틴의 전환은 커널이 아닌 사용자 공간(User Space)에서 이루어지며 OS 입장에서는 스레드가 계속 실행 중인 것처럼 보인다.
코루틴간의 전환은 Thread Context Switching이 아니라 하나의 스레드 내부에서 실행 흐름을 바꾸는 작업에 가까우며 코루틴은 다음과 같은 특징을 가진다.
- OS가 관리하는 스케줄링 대상이 아니다.
- PCB나 TCB(Thread Control Block)를 교체하지 않는다.
- 커널 모드 전환, 타이머 인터럽트, TLB Flush가 발생하지 않는다.
이 덕에 코루틴의 Context Switching은 스레드 Context Switching에 비해 비용이 낮다.
CPU 스케줄링
코루틴이 커널 개입 없이 전환될 수 있는 이유는 CPU 스케줄링 방식의 차이에 있다.
선점형 스케줄링 (Preemptive Scheduling)
프로세스와 스레드는 선점형 스케줄링을 따른다. 이는 OS가 현재 작업 중인 CPU 자원을 강제로 빼앗아 다른 프로세스에 할당할 수 있는 스케줄링을 말한다. 앞서 설명한 타이머 인터럽트 기반 스케줄링은 모두 선점형 스케줄링의 대표적인 예다.
OS는 일정한 시간 간격으로 타이머 인터럽트를 발생시켜 현재 실행 중인 작업을 중단하고 스케줄러를 통해 다음 실행 대상을 선택한다. 이 과정이 매우 짧은 시간 단위로 반복되면서 싱글 코어 CPU 환경에서도 여러 작업이 마치 동시에 실행되는 것처럼 보이게 되며 이를 멀티 태스킹이라 한다. 선점형 스케줄링의 장점은 다음과 같다.
- 공정성(Fairness): 특정 작업이 CPU를 독점하지 못한다.
- 응답성(Responsiveness): UI 스레드가 멈추지 않고 시스템 전체가 굼떠지는 상황을 줄일 수 있다.
하지만 그 대가로 자신이 언제 중단될지 미리 알 수 없고 Context Switching 비용이 발생한다.
비선점형/협력적 스케줄링 (Non-Preemtive / Cooperative Scheduling)
비선점형 스케줄링은 작업이 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) 중단 지점 이후에 필요한 로컬 변수 상태 다음 상태로 전이하기 위한 상태 머신 정보 즉, 코루틴은 실행을 그대로 중단하는 것이 아니라 실행 흐름 자체를 객체로 분리하여 보관하는 방식으로 동작한다.
'CS' 카테고리의 다른 글
| 프로세스와 스레드, 코루틴의 Context Switching의 차이 (1) | 2025.12.20 |
|---|---|
| 자바 프로그램 실행 과정 및 기본 구조 (1) | 2024.01.02 |
| 프로세스와 스레드 (2) | 2023.12.31 |
