Threads: Concepts, Implementation, and Practice
1️⃣Concept of Threads
2️⃣ Implementation of Threads
3️⃣ Process / Thread Practice
1️⃣Concept of Threads
Execution Flows Inside a Process — The Story of Threads
When you open a messenger app, multiple things happen at once. You can type a message, notifications pop up, and a file download continues in the background. So are all these tasks handled by a single execution flow?
What if only one flow existed? You'd have to wait for the file download to finish before sending a message, and the screen would freeze until a notification was processed. The reason we rarely experience such inconvenience is that threads are working behind the scenes, dividing and handling each role.
Today's lesson starts from exactly this point — "How does execution flow split inside a single program?"
프로세스 안에서 흐름이 나뉜다; 스레드(Thread) 이야기
메신저 프로그램을 켜면 동시에 여러 일이 일어난다. 메시지를 입력할 수 있고, 알림이 울리고, 파일 다운로드도 진행된다. 그렇다면 이 모든 작업은 하나의 실행 흐름으로 처리되는 걸까?
만약 하나의 흐름만 존재한다면 어떻게 될까? 파일 다운로드가 끝나야 메시지를 보낼 수 있고, 알림이 처리되기 전까지 화면이 멈춰 있을 것이다. 우리가 프로그램을 쓸 때 이런 불편함을 거의 느끼지 못하는 건, 내부에서 스레드가 역할을 나눠 처리하고 있기 때문이다.
오늘 수업은 바로 이 지점, "하나의 프로그램 안에서 실행 흐름이 어떻게 나뉘는가" 라는 질문에서 출발한다.
The Basic Unit of Execution, and the Flows Within It
In the last session, we covered the process as the basic unit through which the OS manages execution. A process is a running program itself, and the OS allocates memory and resources to each process, managing them independently.
So when execution splits into multiple flows inside a process, how does the OS handle it? Does it manage the whole process as one unit, or does it treat each split flow as a separate entity?
The answer is the latter. The OS distinguishes each individual execution flow inside a process as a thread. In other words, when determining scheduling or execution order, the unit the OS actually works with is not the process, but the thread.
If a process is "the space where a program lives," then a thread is "the actual execution flow moving within that space." This session will carefully work through this relationship — how the OS breaks execution down into manageable units.
실행의 기본 단위, 그리고 그 안에서 나뉘는 흐름
지난 시간에는 운영체제가 실행을 관리하는 기본 단위로 프로세스(Process) 를 다뤘다. 프로세스는 실행 중인 프로그램 그 자체이며, 운영체제는 각 프로세스에 메모리와 자원을 할당하고 독립적으로 관리한다.
그렇다면 프로세스 안에서 실행이 여러 갈래로 나뉠 때, 운영체제는 이를 어떻게 다룰까? 하나의 프로세스로 묶어서 통째로 관리할까, 아니면 나뉜 흐름 각각을 별도의 단위로 구분해서 관리할까?
답은 후자다. 운영체제는 프로세스 내부에서 나뉜 각각의 실행 흐름을 스레드(Thread) 라는 단위로 구분하여 관리한다. 즉, 스케줄링이나 실행 순서를 결정할 때 운영체제가 실질적으로 다루는 단위는 프로세스가 아니라 스레드인 셈이다.
프로세스가 "프로그램이 살아있는 공간"이라면, 스레드는 "그 공간 안에서 실제로 움직이는 실행 흐름"이라고 볼 수 있다. 오늘 수업에서는 바로 이 관계를, 즉 운영체제가 실행을 어떤 단위로 쪼개어 다루는지를 차근차근 정리해나갈 것이다.
What Is a Thread? — An Execution Flow Inside a Process
A thread is the unit of execution flow operating inside a process. More precisely, it is the minimum execution unit that actually receives CPU allocation and runs.
Consider an internet banking application. A user can check their balance while simultaneously requesting a transfer or searching transaction history. All of these functions happen within a s ingle program, but each is handled by a different execution flow. The flow handling the balance, the flow handling the transfer, the flow handling the transaction history — each of these is a thread.
Two key points matter here.
First, one or more threads can exist inside a single process. If a process represents the entire execution environment, a thread is the minimal execution unit actually moving within it.
Second, threads share resources but execute independently. Resources like the code region, global data, and heap are shared among threads within the same process. However, each thread's execution flow proceeds independently.
This is the essence of a thread. Share resources, execute separately.
스레드란 무엇인가? 프로세스 안의 실행 흐름
스레드(Thread)는 프로세스 내부에서 동작하는 실행 흐름의 단위다. 좀 더 정확히 말하면, CPU를 실제로 할당받아 동작하는 최소 실행 단위라고 할 수 있다.
인터넷 뱅킹 프로그램을 예로 들어보자. 사용자는 잔액 조회를 하면서 동시에 이체를 요청하거나 거래 내역을 검색할 수 있다. 이 모든 기능이 하나의 프로그램 안에서 이루어지지만, 각 기능은 서로 다른 실행 흐름으로 처리된다. 잔액을 처리하는 흐름, 이체를 처리하는 흐름, 거래 내역을 처리하는 흐름, 이 각각이 바로 스레드다.
여기서 중요한 점은 두 가지다.
첫째, 하나의 프로세스 안에는 하나 이상의 스레드가 존재할 수 있다. 프로세스가 실행 환경 전체를 의미한다면, 스레드는 그 안에서 실제로 움직이는 최소한의 실행 단위다.
둘째, 스레드는 자원을 공유하되, 실행은 독립적으로 이루어진다. 코드 영역, 전역 데이터, 힙(Heap) 영역과 같은 자원은 같은 프로세스 안의 스레드들이 함께 공유한다. 그러나 각 스레드의 실행 흐름 자체는 서로 독립적으로 진행된다.
이것이 스레드의 핵심이다. 자원은 나눠 쓰고, 실행은 따로 간다.
Single-Thread vs Multi-Thread; Four Execution Structures
The number of processes and the number of threads are independent concepts. Their combination determines the execution structure, which can be broken into four forms.
① Single Process + Single Thread
Only one execution flow exists, so tasks proceed sequentially. One task must finish before the next begins. If one task takes a long time, everything else waits. The simplest structure.
② Single Process + Multiple Threads
One process, but execution splits into multiple flows. One thread handles file downloads while another handles user input; multiple tasks appear to run simultaneously within the same program. The messenger and internet banking examples from earlier fall into this category.
③ Multiple Processes + Single Thread
Multiple processes exist, each with only one execution flow. Running a web browser and a music player at the same time is a typical example. The reason execution appears divided here is not threads, but the fact that multiple independent processes are running.
④ Multiple Processes + Multiple Threads
Multiple processes exist, and each contains multiple threads. For example, running a browser and a messenger simultaneously; the browser runs multiple threads per tab, and the messenger has separate threads for message handling and file downloading. This is the most common structure in modern operating systems.
The number of processes and the number of threads operate on different dimensions. How you combine them determines a program's execution structure.
단일 스레드와 다중 스레드: 실행 구조의 네 가지 형태
프로세스의 수와 스레드의 수는 서로 독립적인 개념이다. 이 두 요소의 조합에 따라 실행 구조가 달라지며, 크게 네 가지 형태로 나눠볼 수 있다.
① 단일 프로세스 + 단일 스레드
실행 흐름이 하나뿐이므로 작업이 순차적으로 진행된다. 한 작업이 끝나야 다음 작업으로 넘어가기 때문에, 어떤 작업이 오래 걸리면 나머지는 그동안 기다려야 한다. 가장 단순한 구조다.
② 단일 프로세스 + 다중 스레드
프로세스는 하나지만 실행 흐름이 여러 갈래로 나뉘어 동작한다. 한 스레드는 파일 다운로드를, 다른 스레드는 사용자 입력을 처리하는 식으로, 같은 프로그램 안에서 여러 작업이 동시에 수행되는 것처럼 보인다. 앞서 살펴본 메신저나 인터넷 뱅킹이 이 구조에 해당한다.
③ 다중 프로세스 + 단일 스레드
프로세스 자체가 여러 개 존재하고, 각 프로세스는 하나의 실행 흐름만 가진다. 웹 브라우저와 음악 재생 프로그램을 동시에 실행한 경우가 대표적인 예다. 이때 실행이 나뉘어 보이는 이유는 스레드 때문이 아니라, 서로 독립된 프로세스가 여러 개 동작하고 있기 때문이다.
④ 다중 프로세스 + 다중 스레드
여러 프로세스가 존재하고, 각 프로세스 안에서도 여러 스레드가 동작하는 구조다. 예를 들어 웹 브라우저와 메신저를 동시에 실행했을 때, 브라우저 안에서는 탭마다 여러 스레드가 돌아가고, 메신저 안에서도 메시지 처리와 파일 다운로드를 담당하는 스레드가 각각 존재한다. 이것이 현대 운영체제에서 가장 일반적으로 사용되는 구조다.
결국 프로세스의 개수와 스레드의 개수는 서로 다른 차원의 개념이다. 이 둘을 어떻게 조합하느냐에 따라 프로그램의 실행 구조가 결정된다.
Process Internal Structure; What Threads Share and What They Keep Separate
A process holds common resources: the code region, global data region, and heap region. In a multi-threaded environment, multiple threads within the same process share these resources.
Consider the following code:
int count = 0;
void process() {
int x = 0;
}
The global variable count is stored in the data region, so all threads within the same process can access it. If two threads simultaneously perform count++, then count becomes a shared resource. The code region, global data, and heap exist at the process level and are shared by all threads.
However, not everything is shared. Each thread independently holds its own stack region. Even if two threads call the same function simultaneously, the local variable x inside that function is created separately on each thread's stack. Thread A's x and Thread B's x occupy different memory locations despite having the same name, so changes in one do not affect the other.
In summary: the code region, global data, and heap are shared between threads, while the stack is independently maintained per thread. This structure allows multiple threads to efficiently share resources while each executes without interfering with the others.
프로세스 내부 구조: 스레드는 무엇을 공유하고 무엇을 따로 가지는가
프로세스는 코드 영역, 전역 데이터 영역, 힙 영역과 같은 공통 자원을 가지고 있다. 다중 스레드 환경에서는 같은 프로세스 안에 있는 여러 스레드가 이 자원들을 함께 사용하게 된다.
예를 들어 아래와 같은 코드가 있다고 해보자.
int count = 0;
void process() {
int x = 0;
}
여기서 전역 변수 count는 데이터 영역에 저장되기 때문에, 같은 프로세스 안의 모든 스레드가 함께 접근할 수 있다. 두 스레드가 동시에 count++ 연산을 수행한다면, count는 두 스레드가 공유하는 자원이 된다. 코드 영역, 전역 데이터, 힙 영역은 이처럼 프로세스 단위로 존재하며 스레드들이 공유한다.
그러나 모든 자원을 공유하는 것은 아니다. 각 스레드는 실행에 필요한 스택(Stack) 영역을 독립적으로 가진다. 예를 들어 두 스레드가 같은 함수를 동시에 호출하더라도, 그 함수 안의 지역 변수 x는 각 스레드의 스택에 따로 생성된다. 스레드 A의 x와 스레드 B의 x는 이름은 같아도 서로 다른 메모리 공간에 존재하기 때문에, 한쪽의 변경이 다른 쪽에 영향을 주지 않는다.
정리하면 다음과 같다. 코드 영역, 전역 데이터, 힙 영역은 스레드 간에 공유되고, 실행 흐름을 위한 스택은 스레드마다 독립적으로 존재한다. 이 구조 덕분에 여러 스레드가 자원을 효율적으로 나눠 쓰면서도, 각자의 실행은 서로 간섭 없이 독립적으로 이루어질 수 있다.
Thread Address Space; A Structure That Shares Yet Separates
When a process runs, the OS allocates a memory space exclusively for that process. Within this space, the code, data, and heap regions are placed at distinct locations. "Distinct locations" means they sit at different addresses — not that they use separate memory. They all exist within the same memory range belonging to one process, just partitioned.
In a multi-threaded environment, multiple threads share this memory space. The same function in the code region can be executed by multiple threads simultaneously, and global variables are accessible to all threads.
Yet each thread has its own separate stack. This means that even when executing the same function concurrently, each thread independently maintains its own parameter values and local variables. One thread's execution cannot affect another thread's local variables.
To summarize: code, data, and heap are shared between threads; the stack exists independently per thread. The ability to share memory while maintaining independent execution flows is made possible by this structure. This shared-yet-separated design is the defining characteristic of a multi-threaded environment.
스레드의 주소 공간: 공유하되 분리된 구조
프로세스가 실행되면 운영체제는 그 프로세스만의 메모리 공간을 할당한다. 이 공간 안에는 코드 영역, 데이터 영역, 힙 영역이 각각 구분된 위치에 배치된다. 여기서 "각각 다른 위치"란 이 영역들이 서로 다른 주소에 놓인다는 뜻이지, 별개의 메모리를 쓴다는 의미가 아니다. 모두 하나의 프로세스에 속한 동일한 메모리 범위 안에 구분되어 존재한다.
다중 스레드 환경에서는 이 메모리 공간을 여러 스레드가 함께 사용한다. 코드 영역에 있는 같은 함수를 여러 스레드가 동시에 실행할 수 있고, 전역 변수 역시 모든 스레드가 함께 접근할 수 있다.
그러나 각 스레드는 자신만의 스택 영역을 따로 가진다. 덕분에 같은 함수를 동시에 실행하더라도 각 스레드는 서로 다른 매개변수 값과 지역 변수를 독립적으로 유지할 수 있다. 한 스레드의 실행이 다른 스레드의 지역 변수에 영향을 주지 않는 것도 이 때문이다.
정리하면, 코드·데이터·힙은 스레드 간에 공유되고, 스택은 스레드마다 독립적으로 존재한다. 같은 메모리를 나눠 쓰면서도 각자의 실행 흐름을 유지할 수 있는 것은 바로 이 구조 덕분이다. 공유하되 분리된 이 구조가 멀티스레드 환경의 핵심 특징이다.
Thread Characteristics; Resource Sharing and Execution Separation
The core characteristics of threads can be summarized in two points.
① Resource Sharing
Threads within the same process share the code, data, and heap regions. In a shopping mall website, one thread fetches the product list, another calculates the cart total, and another handles user input. Since product information and user data all reside in the same memory, there is no need to copy data when passing it between threads, they are all looking at the same memory. This structure enables efficient cooperation between threads inside a process.
② Separation of Execution Flows
Because multiple execution flows can exist within one process, each thread has its own execution order and operates independently. Think of video streaming: one thread downloads video data, another renders it on screen, and another handles user input. The execution is divided, yet all flows run within a single program.
Ultimately, threads operate on two principles: share resources, execute separately.
스레드의 특징; 자원 공유와 실행 흐름의 분리
스레드의 핵심 특징은 크게 두 가지로 정리할 수 있다.
① 자원 공유
같은 프로세스 안에 있는 스레드들은 코드, 데이터, 힙 영역을 함께 사용한다. 쇼핑몰 웹사이트를 예로 들면, 한 스레드는 상품 목록을 불러오고, 다른 스레드는 장바구니를 계산하고, 또 다른 스레드는 사용자 입력을 처리한다. 이때 상품 정보나 사용자 데이터는 모두 같은 메모리에 존재하기 때문에, 스레드 간에 데이터를 주고받을 때 굳이 복사할 필요가 없다. 같은 메모리를 함께 바라보고 있기 때문이다. 이 구조 덕분에 프로세스 내부에서 스레드 간 협력이 효율적으로 이루어질 수 있다.
② 실행 흐름의 분리
하나의 프로세스 안에 여러 실행 흐름이 존재할 수 있기 때문에, 각 스레드는 자신만의 실행 순서를 가지면서 독립적으로 동작한다. 영상 스트리밍을 생각해보면, 한 스레드는 영상 데이터를 다운로드하고, 다른 스레드는 화면에 출력하고, 또 다른 스레드는 사용자 입력을 받는다. 실행은 나뉘어 있지만 이 모든 흐름이 하나의 프로그램 안에서 돌아가고 있는 것이다.
결국 스레드는 자원은 함께 쓰고, 실행은 따로 간다는 두 가지 원칙 위에서 동작한다.
Process vs Thread — A Comparison
Processes and threads are often mentioned together, but they are distinct concepts.
A process represents the entire running program. Each process has its own independent memory space, so running a web browser and Notepad simultaneously means the two programs operate in separate memory spaces with no direct access to each other's memory.
A thread is an execution flow running inside one process. Threads belonging to the same process share memory, and multiple threads can exist within a single process.
In one sentence: a process is the entire execution environment, and a thread is a single execution flow performing actual work within it.
스레드와 프로세스의 비교
프로세스와 스레드는 자주 함께 언급되지만 서로 다른 개념이다.
프로세스는 실행 중인 프로그램 전체를 의미한다. 각 프로세스는 독립적인 메모리 공간을 가지기 때문에, 웹 브라우저와 메모장을 동시에 실행하면 두 프로그램은 서로 다른 메모리 공간에서 각각 동작하며 서로의 메모리에 직접 접근할 수 없다.
스레드는 하나의 프로세스 안에서 실행되는 흐름이다. 같은 프로세스에 속한 스레드들은 메모리를 공유하며, 하나의 프로세스 안에 여러 스레드가 존재할 수 있다.
한 문장으로 정리하면, 프로세스는 실행 환경 전체이고 스레드는 그 내부에서 실제로 작업을 수행하는 하나의 실행 흐름이다.
Why Use Threads?
① Improved Responsiveness
If user input goes unprocessed while a file is uploading, the program feels frozen. But if one thread handles the upload and another handles user input, the screen continues to respond while uploading proceeds. Whether the tasks truly run simultaneously or not, the user perceives the program as uninterrupted.
② Efficiency
Since threads share the same process memory, creating a new thread is far less costly than creating a new process. A new process requires a separate memory allocation, whereas a thread reuses existing memory. When a browser handles multiple tasks, splitting them into threads within one process is far more resource-efficient than spawning a new process for each task.
③ Structural Separation
Threads make it easy to divide work by role within a single program. In a game, one thread handles user input, another handles physics calculations, and another handles rendering. Dividing execution flows by function makes the program's behavior easier to understand and simplifies maintenance; fixing a specific feature only requires modifying its corresponding thread.
Taken together, these three reasons point to the core of threads: the combination of sharing and separation. The ability to share memory while splitting execution independently is the most fundamental reason to use threads — and their greatest defining trait.
스레드를 사용하는 이유
① 응답성 향상
파일을 업로드하는 동안 사용자 입력이 전혀 처리되지 않는다면, 사용자 입장에서는 프로그램이 멈춘 것처럼 느껴진다. 하지만 업로드 작업을 한 스레드가 맡고, 사용자 입력 처리를 다른 스레드가 맡는다면 업로드가 진행되는 동안에도 화면은 계속 반응하게 된다. 실제로 작업이 동시에 이루어지든 아니든, 사용자 입장에서는 프로그램이 멈추지 않은 것처럼 느껴지는 것이다.
② 효율성
스레드는 같은 프로세스의 메모리를 공유하기 때문에, 새로운 프로세스를 생성하는 것보다 부담이 훨씬 적다. 프로세스를 새로 만들면 독립적인 메모리 공간을 별도로 할당해야 하지만, 스레드는 기존 메모리를 그대로 활용한다. 예를 들어 브라우저에서 여러 작업을 처리할 때, 각 작업마다 프로세스를 새로 만드는 것보다 같은 프로세스 안에서 스레드로 나누어 처리하는 것이 자원 사용 면에서 훨씬 효율적이다.
③ 구조적 분리
세 번째 이유는 구조적인 분리다. 스레드를 활용하면 하나의 프로그램 안에서 역할별로 작업을 나누기가 쉬워진다. 게임 프로그램을 예로 들면, 사용자 입력을 처리하는 스레드, 물리 연산을 담당하는 스레드, 화면 출력을 맡는 스레드로 역할을 구분할 수 있다. 이렇게 기능별로 실행 흐름을 나누면 프로그램의 동작 구조를 이해하기 쉬워지고, 특정 기능을 수정하거나 개선할 때도 해당 스레드만 손보면 되기 때문에 유지보수가 훨씬 수월해진다.
지금까지 살펴본 세 가지 이유를 종합하면, 스레드의 핵심은 공유와 분리의 결합에 있다. 같은 메모리를 공유하면서도 실행은 독립적으로 나누어 처리할 수 있는 이 구조가 스레드를 사용하는 가장 근본적인 이유이자, 스레드의 가장 큰 특징이다.
Real-World Examples of Thread Usage
① Video Player
Smooth playback requires multiple tasks to run simultaneously. Processing screen output, audio playback, and user input (play/pause) sequentially in one flow would cause the video to freeze or the audio to cut out. Assigning each function to a separate thread allows video, audio, and input handling to proceed concurrently, keeping the overall experience seamless.
② Game Program
A game must continuously refresh the screen, respond instantly to user input, and simultaneously calculate character positions and collision detection. Doing this sequentially would cause the screen to stutter or responses to lag. Splitting tasks by role into independent execution flows solves this problem.
③ Why Not Just Use Multiple Processes?
Separating these tasks into processes might seem feasible on the surface. However, separate processes have independent memory spaces, requiring inter-process communication (IPC) to share state and data — adding complexity and overhead. For closely related tasks, splitting into threads within one process is far more appropriate, since they can share memory while keeping execution separate.
스레드의 실제 사용 예시
스레드가 왜 필요한지는 실제 프로그램 사례를 보면 더 명확하게 이해할 수 있다.
① 동영상 재생 프로그램
영상이 끊기지 않으려면 여러 작업이 동시에 이루어져야 한다. 화면 출력, 음성 재생, 사용자의 재생·일시정지 입력 처리를 하나의 흐름으로 순차적으로 실행한다면 영상이 멈추거나 소리가 끊기는 문제가 생긴다. 각 기능을 별도의 스레드로 나누어 처리하면 영상, 음성, 입력 처리가 동시에 이루어지면서 전체 흐름이 자연스럽게 유지된다.
② 게임 프로그램
게임은 화면을 끊임없이 갱신하면서, 사용자 입력에 즉각 반응하고, 동시에 캐릭터 위치 계산이나 충돌 처리도 수행해야 한다. 이를 순차적으로 처리하면 화면이 멈추거나 반응이 느려질 수밖에 없다. 역할별로 스레드를 나누어 각 기능을 독립적인 실행 흐름으로 처리하면 이 문제를 해결할 수 있다.
③ 프로세스로 나누면 안 될까?
같은 작업을 프로세스로 분리하는 것도 겉으로는 가능해 보인다. 그러나 프로세스를 분리하면 각각 독립된 메모리 공간을 가지게 되어, 작업 간에 상태 정보를 주고받거나 데이터를 공유하기 위해 별도의 프로세스 간 통신이 필요해진다. 이는 구조를 복잡하게 만들고 관리 부담도 커진다. 서로 밀접하게 연관된 작업이라면 하나의 프로세스 안에서 스레드로 나누어 처리하는 것이 훨씬 적합하다. 같은 메모리를 공유하면서 실행만 분리할 수 있기 때문이다.
Threads and Asynchronous Behavior; Word Processor Example
Consider a word processor. Typing on the keyboard should instantly display characters on screen, images should load in the background, and auto-save should run at regular intervals. Handling all of this sequentially in one flow would cause input delays or make the screen appear frozen.
Threads solve this. Assigning input handling, screen rendering, and auto-save to separate threads lets all three proceed without waiting for each other. The user can keep typing without waiting for auto-save to complete, and the program runs naturally.
This structure — where multiple tasks proceed independently without waiting for each other — is called asynchronous behavior. More precisely, it is closer to concurrent execution via threads, but it carries asynchronous characteristics in the sense that tasks proceed without blocking one another.
스레드로 구현하는 비동기적 동작; 워드 편집기 예시
워드 편집기를 생각해보자. 사용자가 키보드로 입력하면 즉시 화면에 글자가 나타나야 하고, 동시에 이미지 로딩이 이루어지고, 일정 시간마다 자동 저장도 실행된다. 이 작업들을 하나의 흐름으로 순차적으로 처리한다면 입력이 지연되거나 화면이 멈춘 것처럼 보일 수 있다.
이를 해결하는 방법이 바로 스레드다. 입력 처리, 화면 갱신, 자동 저장을 각각 별도의 스레드로 나누면 세 작업이 서로를 기다리지 않고 독립적으로 진행된다. 사용자는 자동 저장이 끝날 때까지 기다릴 필요 없이 계속 타이핑할 수 있고, 프로그램 전체는 자연스럽게 동작한다.
이처럼 여러 작업이 서로를 기다리지 않고 독립적으로 진행되는 구조를 비동기적 동작이라고 부른다. 정확히는 스레드를 통한 동시적 실행에 가깝지만, 작업들이 서로 블로킹 없이 진행된다는 점에서 비동기적 특성을 띤다고 볼 수 있다.
Web Browser and Threads;Another Example of Asynchronous Behavior
A web browser works the same way. It simultaneously fetches data from the network, loads images, and handles user interaction. Processing all of this sequentially in one flow would make the page appear to freeze. Browsers are internally structured to use multiple threads for each task simultaneously, allowing users to experience pages that respond continuously without interruption.
웹 브라우저와 스레드; 비동기적 동작의 또 다른 예
웹 브라우저도 마찬가지다. 브라우저는 네트워크에서 데이터를 받아오고, 이미지를 로딩하고, 사용자와의 상호작용을 처리하는 작업을 동시에 수행한다. 이 모든 작업을 하나의 흐름으로 순차적으로 처리한다면 페이지가 멈춘 것처럼 보일 수 있다. 브라우저는 내부적으로 여러 스레드를 활용해 각 작업을 동시에 처리하도록 구성되어 있고, 그 결과 사용자는 페이지가 끊김 없이 계속 반응하는 것으로 느끼게 된다.
2️⃣ Implementation of Threads
How Does the OS Manage Threads?
We learned earlier that a thread is the minimum execution unit that receives CPU allocation and runs, and that multiple threads can exist within a single process. This naturally leads to the following question.
If multiple threads exist simultaneously, how does the OS determine their execution order, and what information does it use to manage them?
Just as the OS uses a data structure called the PCB (Process Control Block) to manage processes, it must also systematically maintain information about each thread. This section examines that structure; specifically, how the OS actually tracks and manages threads.
운영체제는 스레드를 어떻게 관리할까?
앞서 스레드는 CPU를 할당받아 실행되는 최소 실행 단위이며, 하나의 프로세스 안에 여러 스레드가 존재할 수 있다는 것을 배웠다. 여기서 자연스럽게 다음 질문이 생긴다.
여러 스레드가 동시에 존재한다면, 운영체제는 어떤 기준으로 실행 순서를 정하고 어떤 정보를 바탕으로 스레드를 관리할까?
프로세스를 관리할 때 운영체제가 PCB(Process Control Block)라는 자료구조를 사용했던 것처럼, 스레드를 관리할 때도 운영체제는 각 스레드에 대한 정보를 체계적으로 유지해야 한다. 이번 시간에는 바로 그 구조, 즉 운영체제가 스레드를 실제로 어떻게 파악하고 관리하는지를 살펴본다.
Thread Execution States; Why the OS Tracks Them
When multiple threads exist within a single process, each thread can be in a different execution state. One thread may currently be running on the CPU, another may be waiting for its turn, and yet another may be paused waiting for a specific event.
Because each thread has a different state, the OS must continuously decide which thread to run next and when to hand the CPU over to another thread. In other words, a thread is not simply a flow of execution, it is something the OS must constantly track and manage.
스레드의 실행 상태: 운영체제가 추적하는 이유
하나의 프로세스 안에 여러 스레드가 존재하면, 각 스레드는 서로 다른 실행 상태를 가질 수 있다. 어떤 스레드는 현재 CPU를 사용해 실행 중이고, 어떤 스레드는 실행 순서를 기다리고 있으며, 또 어떤 스레드는 특정 이벤트를 기다리며 멈춰 있을 수 있다.
스레드마다 상태가 다르기 때문에 운영체제는 지금 어떤 스레드를 실행시킬지, 언제 CPU를 다른 스레드에게 넘길지를 계속해서 판단해야 한다. 즉 스레드는 단순히 실행되는 흐름이 아니라, 운영체제가 끊임없이 추적하고 관리해야 하는 대상이 된다.
TCB - The Data Structure for Managing Threads
To manage threads, the OS must know the following about each one: its current state, where execution should resume next, what values are stored in the CPU registers, and where the stack is located.
To store and manage this information, the OS uses the TCB (Thread Control Block). The TCB holds information about a single thread and serves as the management unit used to track and switch between threads.
Just as the PCB exists to manage processes, the TCB exists to manage threads. The OS uses the TCB to determine which thread to run and when, and to restore the necessary information when switching from one thread to another.
TCB: 스레드를 관리하는 자료구조
운영체제가 스레드를 관리하려면 각 스레드에 대해 다음과 같은 정보를 파악하고 있어야 한다. 현재 어떤 상태인지, 다음에 어디서부터 실행을 이어가야 하는지, CPU 레지스터에는 어떤 값이 저장되어 있는지, 스택은 어디에 위치해 있는지가 그것이다.
이 정보를 저장하고 관리하기 위해 운영체제는 TCB(Thread Control Block, 스레드 제어 블록) 를 사용한다. TCB는 스레드 하나에 대한 정보를 담고 있으며, 스레드를 추적하고 전환하기 위해 사용되는 관리 단위다.
프로세스를 관리하기 위해 PCB가 존재하듯, 스레드 단위의 관리를 위해 TCB가 존재하는 것이다. 운영체제는 이 TCB를 바탕으로 어떤 스레드를 언제 실행할지 판단하고, 실행 중인 스레드를 다른 스레드로 전환할 때 필요한 정보를 복원한다.
The Role of the TCB; A Data Structure That Remembers Execution Flow
A thread is an execution flow that receives CPU allocation and runs. The OS must be able to pause this flow and resume it later.
Consider a scenario where Thread A is running, then pauses and switches to Thread B, and eventually returns to Thread A. For Thread A to resume correctly, it must remember exactly where it stopped. To enable this, the OS stores each thread's execution state and the information needed to resume it in the TCB.
Ultimately, the TCB is a data structure that preserves execution flow so that a thread can be resumed at any time. The key takeaway is this: a thread is an execution flow, and the TCB is the structure that remembers it.
TCB의 역할: 실행 흐름을 기억하는 자료구조
스레드는 CPU를 할당받아 실행되는 흐름이다. 운영체제는 이 실행 흐름을 중단했다가 나중에 다시 이어서 실행할 수 있어야 한다.
예를 들어 스레드 A가 실행되다가 멈추고 스레드 B로 전환되었다가 다시 스레드 A로 돌아온다고 해보자. 이때 스레드 A는 어디까지 실행했는지를 기억하고 있어야 정확한 위치부터 이어서 실행할 수 있다. 이를 위해 운영체제는 각 스레드의 실행 상태와 실행을 재개하는 데 필요한 정보를 TCB(Thread Control Block) 에 저장한다.
결국 TCB는 스레드가 언제든 다시 실행될 수 있도록 실행 흐름을 보존하는 자료구조다. 핵심만 짚으면 이렇다. 스레드는 실행 흐름이고, TCB는 그 흐름을 기억하는 구조다.
The Structure of the TCB
Let's look at what information the TCB contains. A process's memory space includes code, data, and heap regions, all of which are shared by every thread within that process. The stack, however, exists separately for each thread. Since each thread has an independent execution flow, the local variables and return addresses generated during function calls are stored on each thread's own stack.
Each thread's TCB holds three key pieces of information:
Thread ID: A unique identifier that distinguishes this thread from others.
PC (Program Counter): Indicates how far the thread has executed.
SP (Stack Pointer): Indicates where the stack is currently pointing.
When a thread is paused and needs to resume, these values must be accurately restored. The PC must be restored so execution can continue from where it stopped, and the SP must be restored so the local variables and return addresses on the stack can be correctly referenced.
In short, the TCB stores everything the OS must know to pause and resume a thread — who this thread is, how far it has executed, and where its stack is.
스레드 제어 블록(TCB)의 구조
TCB가 어떤 정보를 담고 있는지 구조적으로 살펴보자. 프로세스의 메모리 공간에는 코드, 데이터, 힙 영역이 있고, 이 영역들은 프로세스 안의 모든 스레드가 함께 공유한다. 반면 스택은 스레드마다 별도로 존재한다. 각 스레드가 독립적인 실행 흐름을 가지기 때문에, 함수 호출 시 생성되는 지역 변수와 반환 주소도 각자의 스택에 따로 저장된다.
각 스레드의 TCB에는 크게 세 가지 정보가 담긴다.
스레드 ID: 이 스레드가 누구인지를 식별하는 고유 번호다.
PC (Program Counter): 스레드가 현재 어디까지 실행되었는지를 나타낸다.
SP (Stack Pointer): 스택이 현재 어디를 가리키고 있는지를 나타낸다.
스레드가 중단되었다가 다시 실행되려면 이 값들이 정확히 복원되어야 한다. PC가 복원되어야 중단된 위치부터 이어서 실행할 수 있고, SP가 복원되어야 스택에 저장된 지역 변수와 반환 주소를 올바르게 참조할 수 있다.
결국 TCB는 운영체제가 스레드를 멈췄다가 다시 실행하기 위해 반드시 알아야 할 정보, 즉 이 스레드가 누구인지, 어디까지 실행됐는지, 스택은 어디에 있는지를 저장하는 자료구조다.
The Relationship Between PCB and TCB
Once you understand the relationship between processes and threads, the relationship between PCB and TCB follows naturally.
The PCB manages information about a process's resources and its entire execution environment. The TCB stores information about the execution flow of each thread within that process.
Taking a web browser as an example: information about the entire browser process is stored in the PCB, while information about the threads handling individual tabs or tasks is stored in each thread's TCB.
In other words, PCB and TCB form a structure where one PCB is linked to multiple TCBs. If a process is the container of the execution environment, then each thread moving within it is individually tracked and managed by the OS through its own TCB.
PCB와 TCB의 관계
프로세스와 스레드의 관계를 이해했다면, 이를 관리하는 자료구조인 PCB와 TCB의 관계도 자연스럽게 이해할 수 있다.
PCB(Process Control Block) 는 프로세스의 자원과 실행 환경 전체에 대한 정보를 관리한다. TCB(Thread Control Block) 는 그 프로세스 안에 존재하는 각 스레드의 실행 흐름에 대한 정보를 저장한다.
웹 브라우저를 예로 들면, 브라우저 프로세스 전체에 대한 정보는 PCB에 저장되고, 각 탭이나 개별 작업을 처리하는 스레드의 정보는 각각의 TCB에 저장된다.
즉 PCB와 TCB는 하나의 PCB에 여러 개의 TCB가 연결된 구조다. 프로세스가 실행 환경의 그릇이라면, 그 안에서 움직이는 각 스레드는 자신만의 TCB를 통해 운영체제에 의해 개별적으로 추적되고 관리된다.
Distinguishing the Roles of PCB and TCB
The relationship between PCB and TCB can be summarized in one sentence:
The PCB manages a process's resources and execution environment; the TCB manages the threads executing within it.
Process-level management information is handled by the PCB, and thread-level execution information is handled by the TCB. The OS uses both data structures together to manage processes and threads hierarchically. If the PCB holds the big picture, the TCB tracks the fine details of each individual execution flow moving within it.
PCB와 TCB의 역할 구분
PCB와 TCB의 관계를 한 문장으로 정리하면 다음과 같다.
PCB는 프로세스의 자원과 실행 환경을 관리하고, TCB는 그 안에서 실행되는 스레드를 관리한다.
프로세스 단위의 관리 정보는 PCB가 담당하고, 스레드 단위의 실행 정보는 TCB가 담당한다. 운영체제는 이 두 자료구조를 함께 활용해 프로세스와 스레드를 계층적으로 관리한다. PCB가 큰 그림을 잡아준다면, TCB는 그 안에서 실제로 움직이는 각각의 실행 흐름을 세밀하게 추적하는 역할을 한다.
Thread Implementation; It Depends on Who Manages Them
We established earlier that a thread is an execution flow. This raises an important question: the way threads are implemented varies depending on how far the OS recognizes and manages them.
Based on who manages the threads, implementations fall into three categories:
User-Level Threads
Kernel-Level Threads
Hybrid Threads
Let's examine how each works and what distinguishes them.
스레드의 구현 방식: 누가 관리하느냐에 따라 달라진다
앞서 스레드는 실행 흐름이라는 것을 배웠다. 여기서 중요한 질문이 하나 생긴다. 운영체제가 스레드를 어디까지 인식하고 관리하느냐에 따라 스레드의 구현 방식이 달라진다는 것이다.
스레드를 관리하는 주체가 누구냐에 따라 크게 세 가지로 구분할 수 있다.
사용자 수준 스레드
커널 수준 스레드
혼합형 스레드
각각의 방식이 어떤 구조로 동작하고, 어떤 차이가 있는지 하나씩 살펴보자.
Implementation ① — User-Level Threads
In the user-level thread model, thread creation, scheduling, and management are performed not by the OS, but by a thread library. The OS does not recognize the existence of threads at the kernel level and views the process as a single execution unit. Therefore, even if multiple threads are taking turns executing within a process, the OS sees it as one process using one CPU.
The advantages are threefold. First, no kernel call is needed when switching threads, so context switch overhead is low. Second, thread creation and management is fast. Third, applications can freely design their own scheduling policies without depending on the OS.
The disadvantage is significant. Parallel execution in multi-core environments is not possible. Because the OS treats the entire process as a single execution unit, it allocates only one CPU. No matter how many threads exist internally, they cannot run on multiple cores simultaneously.
스레드의 구현 방식 ① 사용자 수준 스레드
사용자 수준 스레드 방식에서는 스레드의 생성, 스케줄링, 관리가 운영체제가 아닌 스레드 라이브러리에 의해 수행된다. 운영체제는 커널 수준에서 스레드의 존재 자체를 인식하지 못하며, 해당 프로세스를 하나의 실행 단위로만 바라본다. 따라서 프로세스 내부에서 여러 스레드가 번갈아 실행되더라도, 운영체제 입장에서는 하나의 프로세스가 하나의 CPU를 사용하는 것처럼 보인다.
장점은 세 가지로 정리할 수 있다. 첫째, 스레드 전환 시 커널 호출이 필요 없기 때문에 문맥 교환 오버헤드가 적다. 둘째, 스레드의 생성과 관리가 빠르다. 셋째, 운영체제에 의존하지 않고 프로그램 내부에서 자체적인 스케줄링을 자유롭게 설계할 수 있다.
반면 단점도 분명하다. 가장 큰 문제는 멀티코어 환경에서 병렬 실행이 어렵다는 점이다. 운영체제는 이 프로세스를 하나의 실행 단위로만 인식하기 때문에 CPU를 하나만 할당한다. 내부에 여러 스레드가 존재하더라도 실제로는 여러 코어에서 동시에 실행되지 못하는 것이다.
N:1 Mapping in User-Level Threads
The structure of user-level threads can be understood from two perspectives.
From a structural position standpoint: the user space sits at the top, the kernel space below it, and hardware at the bottom. Multiple user-level threads exist inside a process in user space, and they are created and scheduled by a thread library in user space — not by the kernel. Crucially, these threads are invisible in the kernel space. The kernel sees the process as a single execution unit.
From a mapping standpoint: multiple user-level threads (N threads) are connected to a single kernel thread. This is called N:1 mapping. Even if there are multiple threads in user space, the kernel sees only one execution flow. If a program contains five threads, the kernel assigns only one kernel thread to the CPU.
This structure makes the previously mentioned disadvantage clear. Even in a multi-core environment, parallel execution across multiple cores is impossible. No matter how many threads exist internally, the kernel's single-unit view prevents simultaneous use of multiple cores.
사용자 수준 스레드의 다대일(N:1) 매핑 구조
사용자 수준 스레드의 구조는 두 가지 관점에서 살펴볼 수 있다.
구조적 위치 관계를 보면, 위쪽에는 사용자 영역, 아래에는 커널 영역, 맨 아래에는 하드웨어 영역이 위치한다. 사용자 영역 안의 프로세스 내부에 여러 개의 사용자 수준 스레드가 존재하며, 이 스레드들은 커널이 아닌 사용자 영역의 스레드 라이브러리에 의해 생성되고 스케줄링된다. 중요한 점은 커널 영역에서는 이 스레드들이 보이지 않는다는 것이다. 커널은 해당 프로세스를 그저 하나의 실행 단위로만 인식한다.
매핑 관계를 보면, 여러 개의 사용자 수준 스레드(N개)가 하나의 커널 스레드에 연결되는 구조를 취한다. 이를 N:1 매핑이라고 부른다. 사용자 영역에는 스레드가 여러 개 존재하더라도, 커널 입장에서는 하나의 실행 흐름으로만 보인다. 프로그램 안에 스레드가 다섯 개 있더라도 커널은 하나의 커널 스레드만 CPU에 할당한다.
이 구조에서 앞서 언급한 단점이 명확하게 드러난다. 멀티코어 환경에서도 여러 코어에 병렬로 실행되지 못한다는 것이다. 커널이 프로세스 전체를 하나의 실행 단위로만 보기 때문에, 내부의 스레드가 아무리 많아도 동시에 여러 코어를 활용할 수 없다.
Implementation ② Kernel-Level Threads
In the kernel-level thread model, the OS directly recognizes and manages threads. Thread creation, scheduling, and state management all occur inside the kernel, and CPU scheduling is performed at the thread level rather than the process level. Each thread is treated as an independent execution unit from the OS's perspective.
There are three advantages. First, if one thread enters a waiting state, other threads can continue running. For example, if a thread is waiting for file I/O, the OS places that thread in a waiting state and assigns the CPU to another thread. Second, parallel processing is possible in multi-core environments, since each thread is independently scheduled by the kernel and can run on multiple CPU cores simultaneously. Third, because the kernel manages resources at the thread level, scheduling and protection are handled consistently.
The disadvantage is that kernel involvement is required every time a thread is created or switched. This means context switch overhead is higher than with user-level threads, and the overall management cost increases due to the need for kernel mode transitions.
스레드의 구현 방식 ② 커널 수준 스레드
커널 수준 스레드 방식에서는 운영체제가 스레드를 직접 인식하고 관리한다. 스레드의 생성, 스케줄링, 상태 관리가 모두 커널 내부에서 이루어지며, CPU 스케줄링도 프로세스 단위가 아닌 스레드 단위로 수행된다. 운영체제 입장에서 각 스레드는 독립적인 실행 단위로 취급된다.
장점은 세 가지다. 첫째, 한 스레드가 대기 상태가 되더라도 다른 스레드는 계속 실행될 수 있다. 예를 들어 한 스레드가 파일 입출력을 기다리고 있다면, 운영체제는 해당 스레드를 대기 상태로 두고 다른 스레드에 CPU를 할당한다. 둘째, 멀티코어 환경에서 병렬 처리가 가능하다. 각 스레드가 커널에 의해 독립적으로 스케줄링되기 때문에 여러 CPU 코어에서 동시에 실행될 수 있다. 셋째, 커널이 스레드 단위로 자원을 관리하기 때문에 스케줄링과 보호가 일관되게 이루어진다.
단점도 존재한다. 스레드를 생성하거나 전환할 때마다 커널의 개입이 필요하기 때문에, 사용자 수준 스레드에 비해 문맥 교환 오버헤드가 크다. 커널 모드 전환이 필요한 만큼 전반적인 관리 비용도 증가한다.
1:1 Mapping in Kernel-Level Threads
Kernel-level threads use a 1:1 mapping structure, where each user thread maps exactly to one kernel thread.
Unlike N:1 mapping, where multiple user threads are bound to a single kernel thread, 1:1 mapping allows the kernel to recognize each thread individually and schedule them independently. This enables true parallel execution in multi-core environments, where multiple threads can be assigned to different cores simultaneously.
The contrast with N:1 is clear. In N:1, the kernel sees the process as a single unit, making parallel execution impossible. In 1:1, the kernel directly tracks every thread, allowing full utilization of multi-core hardware.
커널 수준 스레드의 일대일(1:1) 매핑 구조
커널 수준 스레드는 1:1 매핑 구조를 취한다. 사용자 스레드 하나가 커널 스레드 하나와 정확히 대응되는 구조다.
N:1 매핑에서는 여러 사용자 스레드가 하나의 커널 스레드에 묶여 있었던 것과 달리, 1:1 매핑에서는 커널이 각 스레드를 개별적으로 인식하고 독립적으로 스케줄링할 수 있다. 덕분에 멀티코어 환경에서 여러 스레드가 서로 다른 코어에 동시에 할당되어 진정한 병렬 실행이 가능해진다.
앞서 살펴본 사용자 수준 스레드의 N:1 구조와 비교하면 차이가 명확하다. N:1에서는 커널이 프로세스를 하나의 실행 단위로만 보기 때문에 병렬 실행이 불가능했지만, 1:1 구조에서는 커널이 스레드 하나하나를 직접 파악하고 있기 때문에 멀티코어의 이점을 온전히 활용할 수 있다.
Why Do Modern OSes Use Kernel-Level Threads?; Multi-core and Kernel Threads
Most modern computers use multi-core CPUs. Kernel-level threads demonstrate their true value in this environment. Because the kernel independently recognizes and schedules each thread, multiple threads can be distributed across different CPU cores. This effectively improves parallel processing performance.
This is the most decisive difference between user-level and kernel-level threads. User-level threads cannot take advantage of multi-core hardware because the kernel sees the process as a single unit. Kernel-level threads, on the other hand, can run as many threads simultaneously as there are cores, a far more suitable structure for modern hardware.
왜 현대 운영체제는 커널 수준 스레드를 사용하는가? 멀티코어 환경과 커널 수준 스레드
현대 컴퓨터는 대부분 멀티코어 CPU를 사용한다. 커널 수준 스레드는 이 환경에서 진가를 발휘한다. 커널이 각 스레드를 독립적으로 인식하고 스케줄링하기 때문에, 여러 스레드를 서로 다른 CPU 코어에 분산하여 실행할 수 있다. 그 결과 병렬 처리 성능을 효과적으로 끌어올릴 수 있다.
이것이 사용자 수준 스레드와 커널 수준 스레드의 가장 결정적인 차이다. 사용자 수준 스레드는 커널이 프로세스를 하나의 실행 단위로만 보기 때문에 멀티코어의 이점을 살리지 못하지만, 커널 수준 스레드는 코어 수만큼 스레드를 동시에 실행할 수 있어 현대 하드웨어 환경에 훨씬 적합한 구조다.
Why Do Modern OSes Use Kernel-Level Threads? Additional Reasons
Beyond parallel processing, there are further reasons to use kernel-level threads.
First, if one thread enters a waiting state due to I/O, other threads can keep running. Because the kernel manages state at the thread level, one thread stalling does not freeze the entire program.
Second, because the kernel manages resources and performs CPU scheduling at the thread level, resource protection and execution control are handled consistently.
Third, if an error occurs in a specific thread, the impact on the overall system is reduced, making it much easier to isolate and manage problems.
For these reasons, modern operating systems adopt kernel-level threads as their default structure. From parallel processing performance to stable resource management and fault isolation, kernel-level threads meet all the demands of the modern computing environment.
왜 현대 운영체제는 커널 수준 스레드를 사용하는가? 커널 수준 스레드를 사용하는 이유
앞서 살펴본 병렬 처리 외에도, 커널 수준 스레드를 사용하는 이유는 더 있다.
첫째, 한 스레드가 입출력 때문에 대기 상태가 되더라도 다른 스레드는 계속 실행될 수 있다. 커널이 스레드 단위로 상태를 관리하기 때문에, 하나의 스레드가 멈춰도 프로그램 전체가 멈추지 않는다.
둘째, 커널이 스레드 단위로 자원을 관리하고 CPU 스케줄링을 직접 수행하기 때문에 자원 보호와 실행 제어가 일관되게 이루어진다.
셋째, 특정 스레드에서 오류가 발생하더라도 시스템 전체에 미치는 영향이 줄어들어 문제를 통제하고 관리하기가 훨씬 수월하다.
이러한 이유들로 인해 현대 운영체제는 커널 수준 스레드를 기본 구조로 채택하고 있다. 병렬 처리 성능, 안정적인 자원 관리, 오류 격리까지, 커널 수준 스레드는 현대 컴퓨팅 환경이 요구하는 조건을 고루 충족하는 구조다.
When Are User-Level Threads Still Useful?
If kernel-level threads are the standard in modern OSes, when are user-level threads still worth using? The key is when you want to control threads quickly in user space without kernel involvement.
① When thread creation and switching happen very frequently
Repeated kernel calls increase overhead. Since user-level threads handle switching without a kernel mode transition, they can operate relatively faster in such scenarios.
② When an application wants direct control over thread behavior
When you want to implement a custom scheduling policy tailored to specific task characteristics, a library-level design offers much greater flexibility — without being constrained by the kernel's scheduling approach.
③ When OS-independent implementation is needed
Kernel thread implementations vary across operating systems. User-level threads, being library-based, can maintain relatively consistent behavior across different environments.
In summary, user-level threads are a suitable choice when performance optimization and scheduling flexibility are priorities.
사용자 수준 스레드는 언제 사용할까?
커널 수준 스레드가 현대 운영체제의 기본 구조라면, 사용자 수준 스레드는 언제 유용할까? 핵심은 커널의 개입 없이 사용자 영역에서 빠르게 스레드를 제어하고 싶을 때다.
① 스레드 생성과 전환이 매우 자주 발생하는 경우
커널 호출이 반복될수록 오버헤드가 커진다. 사용자 수준 스레드는 커널 모드 전환 없이 처리되기 때문에 이런 상황에서 상대적으로 빠르게 동작할 수 있다.
② 응용 프로그램이 스레드 동작을 직접 제어하고 싶은 경우
특정 작업 특성에 맞는 스케줄링 정책을 직접 구현하고자 할 때, 라이브러리 수준에서 보다 유연하게 설계할 수 있다. 커널의 스케줄링 방식에 얽매이지 않아도 된다는 것이 장점이다.
③ 운영체제에 독립적인 구현이 필요한 경우
운영체제마다 커널 스레드의 구현 방식이 다를 수 있다. 반면 사용자 수준 스레드는 라이브러리 기반으로 동작하기 때문에 여러 환경에서 비교적 일관된 동작을 유지할 수 있다.
결론적으로 성능 최적화와 제어 유연성이 중요한 상황에서는 사용자 수준 스레드가 적합한 선택이 될 수 있다.
Thread Implementation in Modern OSes Linux and Windows
So what approach do real operating systems take? Looking at two representative OSes, both Linux and Windows operate fundamentally on kernel-level threads.
Linux manages threads as the basic unit of execution, with the kernel directly performing scheduling per thread.
Windows likewise treats threads as the basic unit of execution, clearly distinguishing between processes as resource management units and threads as execution units.
Both operating systems operate on the same principle. Modern OSes clearly separate processes (resource management units) from threads (execution units) and adopt a structure where the kernel directly manages threads. The advantages of kernel-level threads covered earlier — parallel processing performance, stable resource management, and consistent execution control; are directly reflected in the design of real operating systems.
현대 운영체제의 스레드 구현 방식; 리눅스와 윈도우
그렇다면 실제 운영체제는 어떤 방식을 사용할까? 대표적인 두 운영체제인 리눅스와 윈도우를 살펴보면, 둘 다 기본적으로 커널 수준 스레드를 기반으로 동작한다.
리눅스는 스레드를 기본 실행 단위로 관리하며, 스레드마다 커널이 직접 스케줄링을 수행한다.
윈도우 역시 스레드를 기본 실행 단위로 관리한다. 프로세스는 자원 관리 단위, 스레드는 실행 단위로 명확히 구분하여 운영한다.
두 운영체제 모두 같은 원칙 위에서 동작하는 셈이다. 현대 운영체제는 프로세스는 자원 관리 단위, 스레드는 실행 단위로 명확히 구분하고, 커널 수준에서 스레드를 직접 관리하는 구조를 채택하고 있다. 앞서 배운 커널 수준 스레드의 장점, 즉 병렬 처리 성능, 안정적인 자원 관리, 일관된 실행 제어가 실제 운영체제 설계에도 그대로 반영된 것이다.
Why Hybrid (M:N) Threads Are Rarely Used
Hybrid threads combine user-level and kernel-level threads. The goal is to map M user threads to N kernel threads, gaining both the fast switching of user-level threads and the parallel execution of kernel-level threads.
In theory this sounds attractive, but in practice it is rarely used. There are two main reasons.
First, complex coordination between the user-level library and the kernel is required. Second, since scheduling responsibility is split between user space and the kernel, implementation and debugging become extremely difficult.
The attempt to combine the advantages of both approaches ends up making the structure overly complex. And since modern OSes have already adopted kernel-level threads as their default, adequately solving the parallel processing problem; there is little reason to accept the added complexity of a hybrid model.
혼합형(M:N) 스레드가 잘 사용되지 않는 이유
혼합형 스레드는 사용자 수준 스레드와 커널 수준 스레드를 함께 사용하는 방식이다. M개의 사용자 스레드를 N개의 커널 스레드에 연결하여, 사용자 수준의 빠른 전환과 커널 수준의 병렬 실행이라는 두 가지 장점을 동시에 얻는 것이 목표다.
이론적으로는 매력적인 구조처럼 보이지만, 실제로는 잘 사용되지 않는다. 이유는 크게 두 가지다.
첫째, 사용자 수준 라이브러리와 커널 사이의 복잡한 연동이 필요하다. 둘째, 스케줄링 책임이 사용자 영역과 커널에 이중으로 분산되기 때문에 구현과 디버깅이 매우 어려워진다.
결국 두 방식의 장점을 합치려다 오히려 구조가 지나치게 복잡해지는 결과를 낳는다. 현대 운영체제가 커널 수준 스레드를 기본으로 채택하면서 병렬 처리 문제가 충분히 해결된 만큼, 굳이 복잡한 혼합형 구조를 감수할 필요가 없어진 것도 한 이유다.
Comparing the Three Thread Implementation Models
Here is a summary of the three models covered:
User-Level Threads (N:1) managed by a user library. Context switch cost is low and performance is fast, but the entire process can stall during I/O waits, and multi-core utilization is impossible. Rarely used today.
Kernel-Level Threads (1:1) managed directly by the OS kernel. Context switch cost is relatively higher, but other threads can continue running during I/O waits, and multi-core utilization is fully supported. The model adopted by most modern operating systems.
Hybrid Threads (M:N) managed jointly by user space and the kernel. Context switch cost is moderate and multi-core utilization is possible, but implementation complexity is very high. Rarely used in practice.
In conclusion, user-level threads are fast but limited in scalability, while kernel-level threads accept modest overhead in exchange for multi-core utilization and stability. Modern operating systems use the 1:1 kernel-level thread model as their default — the practical sweet spot between performance and reliability.
세 가지 스레드 구현 방식 비교
지금까지 살펴본 세 가지 스레드 구현 방식을 한눈에 정리해보자.
사용자 수준 스레드(N:1) 는 사용자 라이브러리가 스레드를 관리한다. 문맥 교환 비용이 낮아 빠르게 동작하지만, I/O 대기 시 전체 스레드가 정지될 수 있고 멀티코어 활용이 불가능하다. 현재는 거의 사용되지 않는다.
커널 수준 스레드(1:1) 는 운영체제 커널이 직접 스레드를 관리한다. 문맥 교환 비용이 상대적으로 높지만, I/O 대기 시에도 다른 스레드가 계속 실행될 수 있고 멀티코어 활용이 가능하다. 현재 대부분의 운영체제가 채택하고 있는 방식이다.
혼합형 스레드(M:N) 는 사용자와 커널이 함께 스레드를 관리한다. 문맥 교환 비용은 중간 수준이며 멀티코어 활용도 가능하지만, 구현 복잡도가 매우 높아 실제로는 거의 사용되지 않는다.
결론적으로 사용자 수준 스레드는 빠르지만 확장성이 제한적이고, 커널 수준 스레드는 약간의 오버헤드를 감수하는 대신 멀티코어 활용과 안정성을 확보할 수 있다. 현대 운영체제는 이 현실적인 균형점으로 1:1 커널 수준 스레드 모델을 기본 구조로 사용하고 있다.
3️⃣ Process / Thread Practice
Working with Threads Directly in Linux
So far we have examined the concepts and implementation of threads from a theoretical perspective. Now let's directly observe thread creation and execution flow in an Ubuntu environment.
In Linux, threads can be handled using the Pthread (POSIX Thread) library. Through this library, we will confirm the process of creating threads, controlling execution, and waiting for termination through hands-on practice.
Before diving into the exercises, let's first review the core Pthread functions and related concepts that will be used throughout.
리눅스에서 스레드를 직접 다뤄보자
지금까지 스레드의 개념과 구현 방식을 이론적으로 살펴봤다. 이번에는 우분투 환경에서 스레드의 생성과 실행 흐름을 직접 확인해본다.
리눅스에서는 Pthread(POSIX Thread) 라이브러리를 사용해 스레드를 다룰 수 있다. 이 라이브러리를 통해 스레드를 생성하고, 실행을 제어하고, 종료를 대기하는 과정을 실습으로 확인할 것이다.
본격적인 실습에 앞서, 실습 과정에서 사용하게 될 Pthread의 핵심 함수와 관련 개념을 먼저 정리해보자.
Preparation — What Is Pthread?
Pthread (POSIX Thread) is a thread library that follows the POSIX standard, used for handling threads in Linux and Unix environments.
An important point here: the C language itself does not natively include syntax or keywords for threads. Rather than supporting threads directly at the language level, C uses a structure where thread functionality provided by the OS is brought in as a library.
This is exactly why the Pthread library is used when implementing threads in C on Linux. The thread functionality itself is provided by the OS kernel, and Pthread serves as an interface that wraps that functionality so it can be conveniently called from a C program.
실습 준비 - Pthread란 무엇인가?
Pthread(POSIX Thread) 는 POSIX 표준을 따르는 스레드 라이브러리로, 리눅스와 유닉스 환경에서 스레드를 다룰 때 사용하는 방식이다.
여기서 중요한 점이 있다. C 언어 자체에는 스레드를 위한 문법이나 키워드가 기본적으로 존재하지 않는다. 즉 C 언어 차원에서 스레드를 직접 지원하는 것이 아니라, 운영체제가 제공하는 스레드 기능을 라이브러리 형태로 가져와서 사용하는 구조다.
리눅스 환경에서 C로 스레드를 구현할 때 Pthread 라이브러리를 사용하는 것도 바로 이 때문이다. 스레드 기능 자체는 운영체제 커널이 제공하고, Pthread는 그 기능을 C 프로그램에서 편리하게 호출할 수 있도록 감싸놓은 인터페이스인 셈이다.
Pthread Core Function ① — pthread_create()
pthread_create() is the function for creating a new thread. It creates an additional execution flow separate from the existing main flow.
An important point here is that when creating a thread, you also specify the function that thread will execute. In other words, calling pthread_create() does not merely create a thread, the specified function immediately begins running as a new execution flow.
Another key thing to remember is that the main function itself is also a thread. Therefore, the moment pthread_create() is called, both the main thread and the newly created thread simultaneously have their own execution flows. This is the moment when two or more execution flows come into existence within a single process.
Pthread 핵심 함수 ① pthread_create()
pthread_create()는 새로운 스레드를 생성하는 함수다. 기존의 메인 실행 흐름과는 별도로 새로운 실행 흐름을 하나 더 만드는 역할을 한다.
여기서 중요한 점은 스레드를 생성할 때 그 스레드가 실행할 함수를 함께 지정한다는 것이다. 즉 pthread_create()를 호출하면 단순히 스레드만 만드는 것이 아니라, 지정한 함수가 새로운 실행 흐름으로 즉시 실행되기 시작한다.
또 한 가지 기억해야 할 점은 메인 함수 자체도 하나의 스레드라는 것이다. 따라서 pthread_create()를 호출하는 순간, 메인 스레드와 새로 생성된 스레드가 동시에 각자의 실행 흐름을 가지게 된다. 하나의 프로세스 안에서 두 개 이상의 실행 흐름이 만들어지는 순간이 바로 이 시점이다.
Pthread Core Function ② — pthread_exit()
pthread_exit() is the function that terminates the currently running thread. The important point is that only that one thread is terminated. Calling pthread_exit() does not terminate the entire process; termination can be controlled at the thread level.
For example, you can terminate only the thread that has finished its task while letting the remaining threads continue running.
This is clearly distinct from the regular exit() function. While exit() terminates the entire process, pthread_exit() terminates only the calling thread. Think of it as the function to use when fine-grained, per-thread termination control is needed.
Pthread 핵심 함수 ② pthread_exit()
pthread_exit()은 현재 실행 중인 스레드를 종료하는 함수다. 여기서 중요한 점은 스레드 하나만 종료된다는 것이다. pthread_exit()을 호출해도 프로세스 전체가 종료되지 않으며, 스레드 단위로 종료 처리가 가능하다.
예를 들어 특정 작업을 마친 스레드만 종료하고, 나머지 스레드는 계속 실행되도록 만들 수 있다.
이는 일반적인 exit() 함수와 명확히 구분된다. exit()은 프로세스 전체를 종료하지만, pthread_exit()은 호출한 스레드만 종료한다. 스레드 단위의 세밀한 종료 제어가 필요할 때 사용하는 함수라고 이해하면 된다.
Pthread Core Function ③ pthread_join()
pthread_join() is the function that makes the current thread wait until a specified thread has terminated. It is most commonly used when the main thread needs to wait for worker threads to finish.
The reason this function matters is that without calling pthread_join(), the main thread may terminate first, which can force-terminate other threads that are still working. Creating a thread is important, but so is properly waiting for its work to complete.
To summarize, pthread_join() serves two roles: it controls the execution order of threads, and it ensures all tasks complete normally. Think of it as a function that must always be paired with thread creation.
Pthread 핵심 함수 ③ pthread_join()
pthread_join()은 특정 스레드가 종료될 때까지 현재 스레드가 대기하도록 만드는 함수다. 주로 메인 스레드가 작업 스레드의 종료를 기다릴 때 사용한다.
이 함수가 중요한 이유는 pthread_join()을 호출하지 않으면 메인 스레드가 먼저 종료될 수 있고, 그 경우 아직 작업 중인 다른 스레드들이 강제로 종료될 수 있기 때문이다. 스레드를 생성하는 것만큼, 작업이 끝날 때까지 제대로 기다려주는 과정도 중요하다.
정리하면 pthread_join()은 두 가지 역할을 한다. 스레드의 실행 순서를 제어하고, 모든 작업이 정상적으로 마무리될 수 있도록 보장한다. 스레드를 생성했다면 반드시 짝을 이루어 호출해야 하는 함수라고 이해하면 된다.
Pthread Core Concept ④ pthread_t (Thread Identifier)
pthread_t is a unique identifier for distinguishing threads; a thread ID. When a thread is created, the OS assigns it a unique ID, through which a specific thread can be waited on, controlled, or have its state checked.
pthread_t serves as the most fundamental identifier for managing threads in the Pthread library. Functions like pthread_join() and pthread_create() covered earlier all rely on this identifier to specify and control particular threads.
Internally, this identifier is one of the key pieces of information stored in the TCB (Thread Control Block). It is the starting point from which the OS tracks and manages threads.
Pthread 핵심 개념 ④ pthread_t (스레드 식별자)
pthread_t는 스레드를 구분하기 위한 고유 식별자, 즉 스레드 ID다. 스레드를 생성하면 운영체제는 각 스레드를 관리하기 위해 고유한 ID를 부여하며, 이 ID를 통해 특정 스레드를 기다리거나 제어하거나 상태를 확인할 수 있다.
pthread_t는 Pthread 라이브러리에서 스레드를 관리하기 위한 가장 기본적인 식별자 역할을 한다. 앞서 살펴본 pthread_join()이나 pthread_create() 같은 함수들도 모두 이 식별자를 기반으로 특정 스레드를 지정하고 제어한다.
내부적으로 보면 이 식별자는 TCB(스레드 제어 블록)에 저장되는 핵심 정보 중 하나다. 운영체제가 스레드를 추적하고 관리하는 출발점이 바로 이 ID라고 이해하면 된다.
Pthread Core Concept ⑤ Passing Data Between Threads
In Pthread, thread functions always receive an argument of type void *. This means data is passed by address, not by value. Whether passing an integer, a struct, or a string, the memory address where the data is stored is what gets handed to the thread.
Why is it designed this way? To unify the form of all thread functions. Regardless of what type of data is passed, the function receives a single address and reinterprets it as the needed type internally. Threads also return their results as void * upon termination, and when multiple values need to be passed, it is common practice to bundle them into a struct and pass the address of that struct.
There is a critical warning here. Suppose you create a local variable inside a function and pass its address to a thread. When that function returns, the local variable disappears from memory. But the thread may still be running. In this case, the thread ends up using an address pointing to memory that no longer exists, causing an invalid memory access.
Therefore, whenever passing data to a thread, always ask: "Will this memory still be alive when the thread finishes?" The core principle of Pthread data passing is that it works by address rather than by value, and the lifetime and scope of that memory must always be considered.
Pthread 핵심 개념: 스레드 간 데이터 전달
Pthread에서 스레드 함수는 항상 void * 형태의 인자를 받는다. 이는 값 자체를 넘기는 구조가 아닌 주소를 넘기는 구조라는 뜻이다. 정수든 구조체든 문자열이든, 스레드에 데이터를 전달할 때는 그 데이터가 저장된 메모리의 주소를 넘겨주게 된다.
왜 이렇게 설계했을까? 스레드 함수의 형태를 하나로 통일하기 위해서다. 어떤 타입의 데이터가 오더라도 일단 주소 하나를 받고, 함수 내부에서 필요한 타입으로 다시 해석하도록 만든 구조다. 스레드가 종료할 때도 마찬가지로 void * 형태로 결과를 반환하며, 여러 값을 전달할 경우에는 구조체로 묶어서 넘기는 방식을 많이 사용한다.
여기서 반드시 주의해야 할 점이 있다. 어떤 함수 안에서 지역 변수를 만들고 그 주소를 스레드에 넘겼다고 가정해보자. 함수가 끝나면 그 지역 변수는 메모리에서 사라진다. 하지만 스레드는 아직 실행 중일 수 있다. 이 경우 스레드는 이미 사라진 메모리를 가리키는 주소를 사용하게 되고, 결국 잘못된 메모리를 참조하는 문제가 발생한다.
따라서 스레드에 데이터를 넘길 때는 항상 "이 메모리가 스레드가 끝날 때까지 살아있는가?" 를 확인해야 한다. Pthread는 데이터를 값이 아닌 주소로 주고받는 구조이며, 그 메모리의 수명과 범위를 반드시 고려해야 한다는 점이 핵심이다.
Example Code ① pthread_create()
Let's start with the code structure. Since C has no built-in thread syntax, #include <pthread.h> must be included to use thread functionality. thread_func is the function the newly created thread will execute. In Pthread, thread functions are defined with a void * signature. This function simply prints "새로운 스레드 실행 중" (New thread running) and terminates with return NULL. Looking at the main function: pthread_t tid is a variable that stores the identifier for the created thread. After printing "main 스레드 시작" (main thread start), calling pthread_create(&tid, NULL, thread_func, NULL) creates a new execution flow, and the new thread begins executing thread_func. The key here is that this code has no pthread_join(). The main thread creates the new thread and immediately moves to the next line without waiting, printing "main 스레드 종료" (main thread end). This leads to two possible outcomes. First, the main thread may hold the CPU and terminate before the new thread gets a chance to run, in which case only "main 스레드 시작 → main 스레드 종료" is printed. Second, if the OS allocates the CPU to the new thread first, the output becomes "main 스레드 시작 → 새로운 스레드 실행 중 → main 스레드 종료." Which thread runs first is determined by the OS's scheduling. This example demonstrates exactly that. pthread_create() creates a new execution flow, but without pthread_join(), the main thread does not wait — so the output can differ every time the program runs.
Through this hands-on example, you can observe that the order of output results varies between runs.
예제 코드 ① pthread_create()
코드 구조부터 살펴보자. C 언어에는 기본적으로 스레드 문법이 없기 때문에 #include <pthread.h>를 반드시 포함해야 스레드 기능을 사용할 수 있다. thread_func은 새로 생성된 스레드가 실행할 함수다. Pthread에서는 스레드 함수의 형태가 void *로 정해져 있다. 이 함수 안에서는 단순히 "새로운 스레드 실행 중"을 출력하고 return NULL로 종료한다. 메인 함수를 살펴보자. pthread_t tid는 생성된 스레드를 구분하기 위한 식별자를 저장하는 변수다. "main 스레드 시작"을 출력한 뒤, pthread_create(&tid, NULL, thread_func, NULL)을 호출하는 순간 새로운 실행 흐름이 만들어지고, 생성된 스레드는 thread_func을 실행하게 된다. 여기서 핵심은 이 코드에 pthread_join()이 없다는 점이다. 메인 스레드는 새 스레드를 만들기만 하고 끝날 때까지 기다려주지 않은 채 바로 다음 줄로 내려가 "main 스레드 종료"를 출력한다. 이 때문에 실행 결과가 두 가지로 나타날 수 있다. 첫째, 메인 스레드가 계속 CPU를 점유하다가 새 스레드가 실행되기 전에 종료해버리는 경우로, "main 스레드 시작 → main 스레드 종료"만 출력된다. 둘째, 운영체제가 새 스레드에 CPU를 먼저 할당하는 경우로, "main 스레드 시작 → 새로운 스레드 실행 중 → main 스레드 종료" 순으로 출력된다. 어떤 스레드가 먼저 실행될지는 운영체제의 스케줄링이 결정한다. 이 예제는 바로 그 점을 보여준다. pthread_create()는 새로운 실행 흐름을 만들어주지만, pthread_join()이 없으면 메인 스레드는 새 스레드를 기다려주지 않기 때문에 실행할 때마다 출력 결과가 달라질 수 있다.
위의 실습 예제를 통해 실행 결과값의 순서가 달라지는 것을 확인할 수 있다.
Example Code ② pthread_join()
In the previous example, using only pthread_create() meant execution order was not guaranteed. This example adds pthread_join() to clearly show the difference. Looking at thread_func: it loops from 1 to 3, printing "스레드 작업 1, 2, 3" (Thread task 1, 2, 3), pausing for 1 second with sleep(1) between each iteration. In the main function, after creating the thread with pthread_create(), pthread_join(tid, NULL) is called immediately. pthread_join() is the key. This function puts the current thread into a waiting state until the specified thread has fully terminated. The main thread does not move to the next line until the new thread has printed 1, 2, 3 and terminated with return NULL. Only after that is "main 종료" (main end) printed. Therefore, the output is always fixed: "스레드 작업 1 → 스레드 작업 2 → 스레드 작업 3 → main 종료." The difference from the previous example is clear. Without pthread_join(), execution order varies by scheduling; with it, the main thread is guaranteed to wait for the new thread to finish. To summarize: pthread_create() adds an execution flow, and pthread_join() synchronizes that flow. Join means "wait", it is the core function that guarantees execution order between threads.
Compared to the previous example, you can observe the clear difference between waiting and not waiting.
예제 코드 ② pthread_join()
앞선 예제에서는 pthread_create()만 사용했기 때문에 실행 순서가 보장되지 않았다. 이번 예제는 여기에 pthread_join()을 추가해 그 차이를 명확히 보여준다. thread_func을 보면 1부터 3까지 반복하며 "스레드 작업 1, 2, 3"을 출력하고, 각 반복마다 sleep(1)으로 1초씩 잠시 멈춘다. 메인 함수에서는 pthread_create()로 스레드를 생성한 뒤 곧바로 pthread_join(tid, NULL)을 호출한다. pthread_join()이 핵심이다. 이 함수는 지정한 스레드가 완전히 종료될 때까지 현재 스레드를 대기 상태로 만든다. 즉 메인 스레드는 새 스레드가 1, 2, 3을 모두 출력하고 return NULL로 종료될 때까지 다음 줄로 내려가지 않는다. 그 이후에야 비로소 "main 종료"가 출력된다. 따라서 실행 결과는 항상 "스레드 작업 1 → 스레드 작업 2 → 스레드 작업 3 → main 종료" 순서로 고정된다. 앞 예제와 비교하면 차이가 분명하다. pthread_join()이 없으면 실행 순서가 스케줄링에 따라 달라지지만, pthread_join()이 있으면 메인 스레드가 반드시 새 스레드의 종료를 기다린다. 정리하면, pthread_create()는 실행 흐름을 추가하고, pthread_join()은 그 흐름을 동기화한다. join은 "기다려라"는 의미이며, 스레드 간 실행 순서를 보장하는 핵심 함수다.
앞의 예제와 비교했을때 기다림이 있는 경우와 없는 경우의 차이를 확인할 수 있다.
Example Code ③ pthread_join() Not Used
This example is nearly identical to the previous one, but the critical difference is the absence of pthread_join(). The main thread creates the new thread and immediately drops to the next line without waiting, printing "main 종료."
In this situation, the result depends on which thread the OS allocates the CPU to first. If the main thread finishes execution first, only "main 종료" is printed, and the process terminates before the worker thread completes its work. Even if the new thread runs first, the main may terminate in the middle of printing "스레드 작업 1," and only if lucky will all three outputs appear. The result differs every time.
Two reasons explain this behavior. First, the main thread does not wait for the new thread. Second, when the main thread terminates, the process itself may terminate, forcibly ending any still-running threads along with it.
In summary: without pthread_join(), execution order is subject to scheduling, and thread tasks may not complete fully. This example demonstrates exactly why pthread_join() is essential.
Worker thread tasks may not run to completion. This confirms that threads are not automatically synchronized just because they were created.
예제 코드 ③ pthread_join() 미사용
이번 예제는 앞선 코드와 거의 동일하지만 pthread_join()이 없다는 점이 결정적인 차이다. 메인 스레드는 새 스레드를 생성한 뒤 기다리지 않고 곧바로 다음 줄로 내려가 "main 종료"를 출력한다.
이 상황에서는 운영체제가 어떤 스레드에 CPU를 먼저 할당하느냐에 따라 결과가 달라진다. 메인 스레드가 먼저 실행을 마쳐버리면 "main 종료"만 출력되고 프로세스가 종료되면서 작업 스레드가 끝까지 실행되지 못한다. 새 스레드가 먼저 실행되더라도 "스레드 작업 1" 출력 도중 메인이 끝나버릴 수 있고, 운이으면 1, 2, 3이 모두 출력될 수도 있다. 실행할 때마다 결과가 달라지는 것이다.
이런 차이가 생기는 이유는 두 가지다. 첫째, 메인 스레드가 새 스레드를 기다려주지 않기 때문이다. 둘째, 메인 스레드가 종료되면 프로세스 자체가 종료될 수 있어 아직 실행 중인 스레드도 함께 강제 종료될 수 있기 때문이다.
정리하면, pthread_join()이 없으면 실행 순서는 스케줄링에 따라 달라지고, 경우에 따라 스레드 작업이 끝까지 수행되지 못할 수 있다. 이 예제는 바로 그 이유로 pthread_join()이 반드시 필요하다는 것을 보여주는 코드다.
woker작업이 끝까지 실행되지 않을수도 있다. 스레드는 만들었다고해서 자동으로 동기화되진 않는다는 점을 확인할 수 있다.
Example Code ④ pthread_exit()
The key of this example is pthread_exit(NULL). This function terminates only the current thread, not the entire process. The main thread terminates at this point, but the worker thread continues running. Therefore, the printf("이 문장은 출력되지 않음\n") below pthread_exit() is never printed; the main thread has already terminated. Looking at the output, it appears that "main에서 pthread_exit 호출" (pthread_exit called from main) prints first, followed by "worker 스레드 작업 1, 2, 3." But strictly speaking, this order is not completely fixed. After pthread_create(), the worker thread could receive the CPU first. The order of the first line of output can vary by scheduling. What matters, however, is that the moment the main thread calls pthread_exit(), the process as a whole remains alive - so the worker thread is guaranteed to complete tasks 1, 2, and 3 in full. This is the biggest difference from the previous example without pthread_join(). Ending with return 0 can terminate the entire process when main exits, but using pthread_exit() terminates only the main thread while other threads continue running. To summarize: return 0 can terminate the entire process; pthread_exit() terminates only the current thread. It is the function that allows termination to be controlled at the thread level, ensuring other threads complete their work even after main exits first.
Even after main terminates, the worker thread continues printing. Note that the order of the first output line is not guaranteed.
예제 코드 ④ pthread_exit()
이번 예제의 핵심은 pthread_exit(NULL)이다. 이 함수는 프로세스 전체가 아닌 현재 스레드만 종료시킨다. 메인 스레드는 이 시점에서 종료되지만, 워커 스레드는 계속 실행된다. 따라서 pthread_exit() 아래의 printf("이 문장은 출력되지 않음\n")은 실제로 출력되지 않는다. 메인 스레드가 이미 종료되었기 때문이다. 실행 결과를 보면 "main에서 pthread_exit 호출"이 먼저 출력되고 이후 "worker 스레드 작업 1, 2, 3"이 이어지는 것처럼 보이지만, 엄밀히 말하면 이 순서가 완전히 고정된 것은 아니다. pthread_create() 이후 워커 스레드가 먼저 CPU를 할당받을 수도 있기 때문이다. 첫 줄의 출력 순서는 스케줄링에 따라 달라질 수 있다. 그러나 중요한 점은 메인 스레드가 pthread_exit()을 호출하는 순간 프로세스 전체는 유지되기 때문에, 워커 스레드는 반드시 작업 1, 2, 3을 끝까지 수행하게 된다는 것이다. 이것이 앞선 pthread_join() 미사용 예제와의 가장 큰 차이다. join 없이 return 0으로 끝내면 메인 종료 시 프로세스 전체가 종료될 수 있지만, pthread_exit()을 사용하면 메인 스레드만 종료되고 다른 스레드는 계속 실행된다. 정리하면 return 0은 프로세스 전체 종료 가능, pthread_exit()은 현재 스레드만 종료다. pthread_exit()은 스레드 단위로 종료를 제어할 수 있는 함수이며, 메인이 먼저 끝나더라도 다른 스레드의 작업을 끝까지 보장하고 싶을 때 사용한다.
메인이 종료되도 worker 스레드는 계속 출력이 된다. 첫 출력 순서는 보장되지 않는다.
Example Code ⑤ pthread_self()
Earlier examples covered how to create, terminate, and wait for threads. This one focuses on how to identify a thread.
pthread_self() is a function that returns the ID of the currently running thread. Calling it inside the main function prints the ID of the main thread. After creating a worker thread with pthread_create(), calling pthread_self() inside the worker thread prints that thread's own ID.
The output shows that the main thread's ID and the worker thread's ID are different. This means each thread holds an independent identifier, and the OS manages threads based on these IDs.
The reason for casting to unsigned long is that pthread_t may not be internally identical to a standard integer type, so the cast is needed to print it in a readable numeric format.
One more important point: the main function is also a thread. We tend to think of main simply as the program's entry point, but from Pthread's perspective, main is a thread with its own unique ID.
To summarize, pthread_self() returns the ID of the currently running thread, and this example confirms that every thread — including main — holds a distinct unique ID.
예제 코드 ⑤ 분석 pthread_self()
앞에서는 스레드를 생성하고, 종료하고, 기다리는 방법을 살펴봤다. 이번에는 스레드를 식별하는 방법을 확인해보자. pthread_self()는 현재 실행 중인 스레드의 ID를 반환하는 함수다. 예제에서 메인 함수 안에서 이를 호출하면 현재 실행 중인 메인 스레드의 ID가 출력된다. 이후 pthread_create()로 워커 스레드를 생성하면, 워커 스레드 안에서도 pthread_self()를 통해 자신의 ID를 출력한다. 실행 결과를 보면 메인 스레드의 ID와 워커 스레드의 ID가 서로 다른 것을 확인할 수 있다. 이것은 각 스레드가 독립적인 식별자를 가지고 있으며, 운영체제가 이 ID를 기준으로 스레드를 관리한다는 것을 의미한다. 여기서 unsigned long으로 형변환을 하는 이유는, pthread_t가 내부적으로 정수형과 완전히 동일하지 않을 수 있기 때문에 출력을 위해 정수형으로 변환해주는 것이다. 또 한 가지 중요한 점은 메인도 하나의 스레드라는 것이다. 우리는 보통 메인을 프로그램의 시작점으로만 생각하지만, Pthread의 관점에서 보면 메인도 고유한 ID를 가진 하나의 스레드다. 정리하면, pthread_self()는 현재 실행 중인 스레드의 ID를 반환하는 함수이며, 메인을 포함한 각 스레드는 서로 다른 고유한 ID를 가진다는 점을 확인할 수 있는 예제다.


