Thread

Process(Fork) vs. Thread
Process
- 실행 중인 프로그램 Program in execution
- 고유한 메모리 영역, 시스템 리소스, 그리고 상태를 가진다.
메모리 내 격리성(isolation)과 병렬 실행 능력 덕분에 안정적으로 장기적 연산을 요구하는 CPU 관련 작업에 적합하다.
Fork
- 현재 실행 중인 프로세스(부모)를 복제(clone)해서 새로운 프로세스(자식)를 만드는 시스템 콜(system call)이다.
- 새로운 프로세스를 만들고, 메모리 공간이 분리된다.
- 독립적이라 안전하지만, context-switching 비용이 크다.
Windows에서는 프로세스가 쓰레드보다 무거운 게 사실이다.
하지만 Linux는 커널에서 프로세스와 쓰레드가 구별되어 있지 않아서 메모리의 공유 여부만 따질 뿐 무게 차이가 전혀 없다.
Thread (Thread)
- 프로세스 내 경량 실행 단위
- 단일 프로세스 내에서 실행 흐름만 분리한다.
- 메모리 공유가 쉬워서 빠르지만, 동기화 문제가 발생할 수 있다.
프로그래밍 언어 수준에서 동기화 문제가 발생하는 걸 방지할 수 있는데, 대표적인(유일한) 예시가 Rust이다.
경량성(lightweightness)과 비차단(non-blocking) 동시성 덕분에 동시성과 빠른 응답을 요구하는 IO 관련 작업에 적합하다.
Kotlin의 Thread는 Java의 Thread를 그대로 사용하기 때문에 동작 원리가 동일하다.

Concurrency vs. Parallelism
동시성(Concurrency)은 여러 작업을 동시에 진행하는 것처럼 보이게 하는 것이다. 이는 단일 코어 CPU에서 여러 작업을 번갈아가며 실행하는 시간 분할(time-sharing) 방식으로 구현된다.
예를 들어, 한 컴퓨터에서 웹 브라우징과 음악 스트리밍을 동시에 진행하는 것은 동시성이다. 실제로는 CPU가 매우 빠르게 두 작업을 오가며 처리하는 것이다.
병렬성(Parallelism)은 여러 작업을 물리적으로 동시에 처리하는 것이다. 이는 멀티 코어 CPU나 멀티 프로세서 시스템에서 각 코어가 다른 작업을 전담하여 처리할 때 발생한다.
예를 들어, 듀얼 코어 CPU에서 한 코어는 게임을, 다른 코어는 바이러스 검사를 동시에 진행하는 것이 병렬성이다.
정리
| 개념 | 주요 목표 | 하드웨어 의존성 | 구현 수단 (일반적) |
| 동시성 (Concurrency) | 시스템의 응답성과 작업 관리 | 없음 (단일 코어에서 가능) | - Multi-threading - Coroutine - Event-loop |
| 병렬성 (Parallelism) | 작업 처리 속도 및 처리량 극대화 | 있음 (멀티 코어 필수) | - Multi-processing - Multi-threading (in Multi-core) |
Thread Methods : run() vs. start()
- run()
- Thread 객체 안에 정의한 run() 메서드를 일반 메서드 호출로 실행할 뿐.
- 따라서 새로운 thread가 생성되지 않고, 호출한 현재 thread에서 그대로 실행된다.
- 즉, main()에서 run()을 호출하면, 병렬 실행 없이 main thread 안에서 동작한다.
- start()
- Thread 클래스 내부적으로 새로운 OS 수준의 thread를 생성하고, 그 안에서 run() 메서드를 실행한다.
- start()를 호출해야 진짜 병렬 실행이 일어난다.
class Worker(private val id: Int) : Thread() {
override fun run() {
for (i in 1..5) {
println("Worker $id - $i (running in ${Thread.currentThread().name})")
Thread.sleep(500) // 0.5초 대기
}
}
}
fun main() {
println("Main thread: ${Thread.currentThread().name}")
val t1 = Worker(1)
val t2 = Worker(2)
val startRun = System.currentTimeMillis()
t1.run()
t2.run()
val endRun = System.currentTimeMillis()
println("run() execution time : ${endRun - startRun} ms")
val t3 = Worker(3)
val t4 = Worker(4)
val startStart = System.currentTimeMillis()
t3.start()
t4.start()
t3.join()
t4.join()
val endStart = System.currentTimeMillis()
println("start() execution time : ${endStart - startStart } ms")
}
/*
Main thread: main
Worker 1 - 1 (running in main)
Worker 1 - 2 (running in main)
..
Worker 1 - 5 (running in main)
Worker 2 - 1 (running in main)
Worker 2 - 2 (running in main)
..
Worker 2 - 5 (running in main)
run() execution time : 5012 ms
Worker 3 - 1 (running in Thread-2)
Worker 4 - 1 (running in Thread-3)
Worker 3 - 2 (running in Thread-2)
Worker 4 - 2 (running in Thread-3)
..
Worker 3 - 5 (running in Thread-2)
Worker 4 - 5 (running in Thread-3)
start() execution time : 2507 ms
*/
정리
- run()
- 단순 함수 실행 (동기 실행)
- main thread에서 순차 실행, 총 10회 * 0.5 = 약 5초
- start()
- 새로운 thread를 만들어서 run() 실행 (비동기 실행)
- 서로 다른 thread에서 실행되므로, 실제 실행 시간은 약 2.5초, 절반 수준으로 떨어진다.
즉, 실제로 multi-threading을 구현하고자 한다면 반드시 start()를 써야 한다.
※ run()은 주로 테스트 용도나 단순 메서드 실행이 필요할 때만 사용된다.
대부분의 환경에서는 쓰레드 클래스를 상속하면 그걸 밖에서 호출하지 못하게 막아놓는 편이다.
다만 Java는 문법이 부족할 때부터 쓰레드를 지원했기 때문에 방어 장치가 부족한 편이다.
C++의 경우, 쓰레드의 내부적 호출 자체를 막아놨다.
Lambda가 생긴 이후로는 이런 식의 실행은 잘 안한다.
Runnable 을 이용한 Thread 생성
Runnable은 Java의 Thread를 구현하기 위한 인터페이스다. 이 인터페이스는 단 하나의 메서드, run() 을 가진다.
Runnable을 구현하는 클래스는 run() 메서드 내부에 실행될 코드를 정의하며, 이 코드가 별도의 thread에서 실행될 작업이 된다.
즉, Runnable은 실행 가능한 작업을 나타내는 '작업 단위(unit of work)'라고 할 수 있다.
Thread에서 Runnable은 다음의 경우 사용한다.
- 클래스 다중 상속 문제 회피
- Kotlin / Java는 클래스 다중 상속이 불가능하다.
- 이미 다른 클래스를 상속받은 클래스도 Runnable 인터페이스를 구현하여 Thread 기능을 추가할 수 있다.
- 유연성과 재사용성
- Thread 자체와 이 thread가 수행할 작업을 분리한다.
- 즉, Runnable은 "해야 할 일"만 정의하고, 그 실행은 각 thread 객체가 담당한다.
- 하나의 Runnable 객체를 여러 Thread에서 실행할 수 있다.
class MyTask (private val name: String) : Runnable {
override fun run() {
for (i in 1..3) {
println("[$name] running $i on ${Thread.currentThread().name}")
Thread.sleep(500)
}
}
}
fun main() {
val t1 = Thread(MyTask("Task-1"))
val t2 = Thread(MyTask("Task-2"))
t1.start()
t2.start()
}
/*
[Task-2] running 1 on Thread-1
[Task-1] running 1 on Thread-0
[Task-1] running 2 on Thread-0
[Task-2] running 2 on Thread-1
[Task-1] running 3 on Thread-0
[Task-2] running 3 on Thread-1
*/
정리
- Runnable은 thread 로직을 캡슐화해서 유연하게 사용할 수 있다.
- Thread.start()와 결합하여 multi-threading을 구현할 수 있다.
- 보통 Thread 상속 보다 Runnable 구현이 보편적이다.
Lambda가 없던 시절에는 이런 식으로 사용하긴 했었다.
이 또한 현재는 잘 사용되지 않는 방식이다.
참고 자료
- Process vs Thread. Process | by BuketSenturk | Medium
- https://techdifferences.com/difference-between-concurrency-and-parallelism.html
- [Java] Thread와 Runnable에 대한 이해 및 사용법 - MangKyu's Diary
Powered By. ChatGPT & Gemini
'Backend' 카테고리의 다른 글
| [Backend] ThreadLocal과 Atomic (1) | 2025.09.22 |
|---|---|
| [Backend] 경쟁 상태와 락 (Race Condition & Lock) (1) | 2025.09.19 |
| [Backend] 데이터 구조 (Queue) (0) | 2025.09.11 |
| [Backend] 데이터 구조 (Stack) (0) | 2025.09.11 |
| [Backend] 데이터 구조 (HashTable) (0) | 2025.09.11 |