1. 프롤로그 - 스레드를 늘렸더니 오히려 느려졌다
동시 접속이 많아지면 "스레드를 더 늘리면 되는 거 아닌가?"라는 생각이 자연스럽게 든다. 그런데 스레드 풀 크기를 늘렸더니 오히려 응답 속도가 더 느려지는 경우가 있다. "더 많은 스레드가 더 빠를 것 같은데, 왜 이런 거지?"
이 질문이 핵심을 짚는다. "지금 병목이 CPU 바운드인가요, I/O 바운드인가요? 그리고 이 작업에는 프로세스가 나을까요, 스레드가 나을까요?"
단순한 정의 암기로는 이 질문에 답할 수 없다. 개발자로서 꼭 알아야 할 "메모리 공유", "안정성", 그리고 "비용(Cost)"의 관점을 이해해야 한다. 오늘은 이 차이를 공장(Factory)과 일꾼(Worker)의 비유로 정리해본다.
2. 비유 - 거대한 공장 vs 성실한 일꾼
이 개념을 가장 직관적으로 이해하는 방법은 "삼성전자 반도체 공장"을 상상하는 것입니다.
프로세스 (Process) = "독립된 공장" (Factory)
- 정의: 운영체제로부터 자원을 할당받은 실행 중인 프로그램.
- 상황: 삼성전자 평택 1공장.
- 특징:
- 완벽한 독립성: 평택 공장에서 가스 폭발 사고가 나도(Crash), 옆에 있는 이천 하이닉스 공장은 멀쩡합니다. 서로 남남입니다.
- 자원 소유: 공장 자체의 주소(Address Space), 기계(Memory), 원자재(Data)를 독점적으로 가집니다.
- 통신 비용(IPC): 평택 공장에서 이천 공장으로 자재를 보내려면? 트럭을 불러서 고속도로를 타야 합니다. 엄청나게 느리고 절차가 복잡합니다.
스레드 (Thread) = "공장 안의 일꾼" (Worker)
- 정의: 프로세스 내에서 실행되는 흐름의 단위.
- 상황: 평택 공장 2층에서 일하는 작업자 김씨, 이씨.
- 특징:
- 자원 공유: 작업자들은 공장의 식당(Heap), 화장실(Data), 기계(Code)를 다 같이 씁니다.
- 통신 효율: 김씨가 이씨에게 "망치 좀 줘" 하려면? 그냥 말하면 됩니다. (메모리 주소 직접 참조). 속도가 빛의 속도입니다.
- 치명적 위험성: 김씨가 실수로 공장 메인 전원을 내려버리면? 공장 안에 있는 이씨, 박씨, 최씨 다 같이 해고당하거나 죽습니다. (프로세스 전체 종료).
이 비유가 머릿속에 확 들어왔다. 이후로는 "프로세스는 격리, 스레드는 공유"라는 핵심이 자연스럽게 이해됐다.
3. 메모리 구조 자세히 살펴보기 (핵심 정리)
실제로 "메모리 구조에서 무슨 차이가 있죠?"라는 질문을 받으면 이걸 화이트보드에 그려서 설명하면 명확합니다.
프로세스의 메모리 구조
각 프로세스는 운영체제로부터 4가지 영역을 모두 독립적으로 할당받습니다. 누구랑도 나누지 않습니다.
- Code (Text): 실행할 기계어 코드가 저장된 영역.
- Data: 전역 변수(Global Variable), 정적 변수(Static)가 저장되는 곳.
- Heap: 런타임에 동적으로 할당되는 메모리 (
malloc,new). - Stack: 함수 호출 시 지역 변수와 매개변수가 저장되는 곳.
스레드의 메모리 구조
스레드는 프로세스 안에 살고 있기 때문에, 효율성을 위해 "Stack만 따로 쓰고 나머지는 다 공유"합니다.
- 공유 영역: Code, Data, Heap. (그래서 통신이 빠름)
- 독립 영역: Stack, PC(Program Counter) 레지스터. (각자 다른 함수를 실행해야 하니까)
핵심 질문: "스레드는 왜 Stack을 따로 갖나요?" 답변: 스레드는 독립적인 실행 흐름이기 때문에, 독립적인 함수 호출 기록(Call Stack)이 필요하기 때문입니다. Stack을 공유하면 A 스레드가 함수를 호출했는데 B 스레드가 리턴받는 말도 안 되는 상황이 벌어집니다.
처음엔 "왜 Stack만 따로 쓰지?"가 이해가 안 갔는데, 함수 호출 스택이라는 개념을 다시 생각해보니까 완전히 받아들였다. 각자의 실행 흐름을 추적하려면 독립된 Stack이 필수였던 거다.
4. 문맥 전환 (Context Switching) - 비용의 차이
"컨텍스트 스위칭이 뭔가요?" CPU가 실행 중인 프로세스/스레드를 멈추고 다른 녀석으로 갈아타는 과정입니다. 여기서 비용 차이가 발생합니다.
프로세스 컨텍스트 스위칭 (이사 가기)
- 비유: 공장 전체를 이사 가는 것.
- 작업: CPU의 캐시 메모리(L1/L2/L3)를 싹 다 비워야 합니다(Flush). TLB(가상 메모리 주소 변환 캐시)도 초기화해야 합니다.
- 비용: 엄청나게 비쌉니다. 오버헤드가 큽니다.
스레드 컨텍스트 스위칭 (작업자 교대)
- 비유: 공장 안에서 작업자만 교대하는 것.
- 작업: 공장은 그대로(메모리 공유) 두고, 작업자의 Stack과 레지스터 값만 살짝 바꾸면 됩니다. 캐시를 비울 필요가 없습니다.
- 비용: 훨씬 저렴하고 빠릅니다. 이것이 웹 서버들이 멀티 프로세스 대신 멀티 스레드를 선호하는 이유입니다.
결국 성능 차이는 이 컨텍스트 스위칭 비용에서 나왔다. 웹 서버에서 수천 개의 요청을 처리할 때, 프로세스 기반으로 하면 CPU가 캐시 비우느라 정신없고, 스레드 기반으로 하면 레지스터만 바꾸면 되니까 훨씬 가볍다는 게 와닿았다.
5. 실제 사례 - 크롬(Chrome)의 비밀 (Site Isolation)
여러분이 매일 쓰는 브라우저에도 이 철학이 담겨 있습니다.
과거의 브라우저 (싱글 프로세스)
옛날 익스플로러(IE)는 브라우저 전체가 하나의 거대한 프로세스였습니다.
- 문제: 탭 하나에서 무거운 사이트가 돌다가 멈추면(Crash), 브라우저 전체가 꺼졌습니다. 10개의 탭이 다 날아갔죠.
크롬 (멀티 프로세스 아키텍처)
구글 크롬은 "탭 하나하나를 별도의 프로세스로 만들자!"는 혁신을 도입했습니다.
- 안정성: 유튜브 탭이 멈춰도, 지메일 탭은 멀쩡합니다. 공장이 다르니까요.
- 보안 (Site Isolation): 2018년 Spectre/Meltdown 취약점 이후, 크롬은 보안을 위해 프로세스 격리를 더욱 강화했습니다. 사악한 사이트가 여러분의 은행 사이트 메모리를 훔쳐보는 것을 물리적으로 막기 위해서입니다.
- 단점: 메모리를 엄청 먹습니다. 탭마다 공장(Code/Data/Heap)을 새로 지으니까요. "크롬이 RAM 먹는 하마"라는 밈은 안정성과 보안을 위한 비용이었습니다.
크롬이 메모리를 왜 그렇게 먹는지 몰랐는데, 이 아키텍처를 이해하고 나니까 "아, 트레이드오프구나"라는 생각이 들었다. 안정성과 보안을 위해 메모리를 희생한 선택이었다는 걸 받아들였다.
6. 파이썬에서 보는 실제 예시: multiprocessing vs threading
파이썬 코드로 직접 확인해본다.
Threading 예시 (스레드 - 메모리 공유)
import threading
counter = 0 # 공유 자원
def worker():
global counter
for _ in range(100000):
counter += 1 # 같은 메모리 접근
threads = []
for _ in range(4):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Result: {counter}") # 400000이 아닐 수 있음! (Race Condition)
결과: 예상과 다른 값이 나옵니다. 여러 스레드가 같은 메모리를 동시에 건드려서 Race Condition 발생.
Multiprocessing 예시 (프로세스 - 메모리 격리)
import multiprocessing
def worker(shared_value):
for _ in range(100000):
with shared_value.get_lock(): # 명시적 Lock 필요
shared_value.value += 1
if __name__ == '__main__':
shared_value = multiprocessing.Value('i', 0) # 공유 메모리 생성
processes = []
for _ in range(4):
p = multiprocessing.Process(target=worker, args=(shared_value,))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Result: {shared_value.value}") # 정확히 400000
핵심 차이점:
- Threading: 메모리를 공유하니까 간단하지만, Race Condition 위험.
- Multiprocessing: 각 프로세스가 독립적이라 안전하지만, 공유 메모리를 명시적으로 만들어야 함.
이 코드를 직접 짜보고 나서야 "아, 그래서 멀티스레드에는 항상 Lock이 필요했구나"가 와닿았다.
7. async/await: 스레드도 아니고 프로세스도 아닌 제3의 길
여기서 더 깊게 들어가면 현대 웹 개발의 핵심인 비동기 프로그래밍이 나옵니다.
이벤트 루프 (Event Loop) 모델
Node.js, Python asyncio는 싱글 스레드로 수천 개의 요청을 처리합니다. 어떻게?
import asyncio
async def task(name, delay):
print(f"{name} 시작")
await asyncio.sleep(delay) # I/O 대기 (CPU 안 씀)
print(f"{name} 완료")
async def main():
await asyncio.gather(
task("작업1", 2),
task("작업2", 1),
task("작업3", 3)
)
asyncio.run(main())
출력:
작업1 시작
작업2 시작
작업3 시작
작업2 완료 # 1초 후
작업1 완료 # 2초 후
작업3 완료 # 3초 후
핵심: CPU를 쓰지 않는 I/O 대기 시간에는 다른 작업으로 전환합니다. 컨텍스트 스위칭 없이 협력적으로 작동합니다.
언제 쓰나?
- CPU Bound 작업 (이미지 처리, 머신러닝): Multiprocessing
- I/O Bound 작업 (웹 크롤링, API 요청): async/await
- 혼합: Threading (적당한 중간)
처음엔 "비동기가 뭐가 빠르지?"라고 생각했는데, I/O 대기 중에도 CPU를 놀리지 않는다는 개념이 이해됐을 때 "결국 이거였다"는 생각이 들었다.
8. 코루틴 (Coroutine) - 가벼운 스레드의 진화
Python asyncio의 코루틴
import asyncio
async def fetch_user(user_id):
print(f"Fetching user {user_id}...")
await asyncio.sleep(1) # DB 조회 시뮬레이션
return f"User{user_id}"
async def main():
users = await asyncio.gather(
fetch_user(1),
fetch_user(2),
fetch_user(3)
)
print(users)
asyncio.run(main())
특징:
- 스레드보다 훨씬 가벼움 (메모리 몇 KB)
- await로 명시적 양보(yield)
- Python의 이벤트 루프가 스케줄링
Kotlin Coroutines (실제 사례)
import kotlinx.coroutines.*
suspend fun fetchUser(userId: Int): String {
delay(1000) // 비동기 대기
return "User$userId"
}
fun main() = runBlocking {
val users = listOf(1, 2, 3).map { userId ->
async { fetchUser(userId) }
}.awaitAll()
println(users) // [User1, User2, User3]
}
코틀린 코루틴의 마법:
- Structured Concurrency: 부모 코루틴이 취소되면 자식도 자동 취소
- suspend 함수: 일시정지 가능한 함수 (마치 게임 세이브포인트)
안드로이드 앱에서 네트워크 요청에 코루틴을 쓰면 스레드보다 훨씬 직관적이다. "일시정지"라는 개념이 코드로 표현된다는 게 흥미롭다.
9. 커널 스레드 vs 사용자 스레드 (Green Thread) 깊이 들여다보기
여기서 더 깊게 들어가면 "고수" 소리를 듣습니다. 스레드라고 다 같은 스레드가 아닙니다.
1) 커널 스레드 (Native Thread) - 1:1 모델
- 주체: 운영체제(OS)가 직접 관리하는 스레드. (Java의 일반 스레드, C++
std::thread). - 특징: OS 스케줄러가 알아서 코어에 배분해줍니다.
- 단점: 생성 비용이 비싸고, 컨텍스트 스위칭 시 커널 모드(Kernel Mode)로 넘어가야 해서 무겁습니다.
2) 사용자 스레드 (Green Thread) - M:N 모델
- 주체: 언어 차원(런타임)에서 만든 가상의 스레드. (Go의 Goroutine, Java 21의 Virtual Thread).
- 특징: OS는 스레드 1개라고 생각하는데, 그 안에서 언어 런타임이 수천 개의 가상 스레드를 돌립니다.
- 장점: 생성 비용이 거의 공짜입니다. 스위칭이 사용자 모드에서 일어나서 엄청 빠릅니다. 고루틴이 수만 개를 띄워도 끄떡없는 이유입니다.
- 비유: 정규직(커널 스레드) 대신 수많은 알바생(그린 스레드)을 고용해서 유연하게 굴리는 것.
10. IPC (Inter-Process Communication): 프로세스 간 대화
프로세스는 서로 남남(독립적)이라 대화하기가 어렵습니다. 그래서 OS가 제공하는 특별한 전화기들이 필요합니다. 이를 IPC라고 합니다.
- 파이프 (Pipe): 단방향 통신. 부모-자식 간에 주로 씀. (Shell의
|생각하면 됨). - 소켓 (Socket): 네트워크 통신. (다른 컴퓨터와 대화 가능). 가장 느리지만 가장 범용적임.
- 공유 메모리 (Shared Memory): 메모리 일부를 억지로 공유하게 만듦. 가장 빠르지만 동기화 문제 발생.
- 메시지 큐 (Message Queue): 우체통에 편지 넣듯이 데이터를 쌓아둠.
반면 스레드는? IPC 따위 필요 없습니다.
그냥 전역 변수 int global_data 선언해놓고 같이 쓰면 됩니다. 통신 비용 Zero.
11. 코드로 보는 차이 (C언어)
백문이 불여일견. 코드로 직접 확인해봤다.
프로세스 생성 (fork) - 복사본 만들기
#include <unistd.h>
#include <stdio.h>
int main() {
int x = 10;
pid_t pid = fork(); // 프로세스 복제 (공장 하나 더 건설!)
if (pid == 0) {
// 자식 프로세스
x = 20;
printf("Child: x = %d\n", x); // 20
} else {
// 부모 프로세스
sleep(1);
printf("Parent: x = %d\n", x); // 10
}
}
결과: 자식이 x를 20으로 바꿔도, 부모는 여전히 10입니다. 메모리가 복사되어 분리되었기 때문입니다.
스레드 생성 (pthread_create) - 공유하기
#include <pthread.h>
#include <stdio.h>
int x = 10; // 전역 변수 (공유 자원)
void* worker(void* arg) {
x = 20; // 다 같이 쓰는 x를 바꿈!
return NULL;
}
int main() {
pthread_t t;
pthread_create(&t, NULL, worker, NULL); // 일꾼 고용
pthread_join(t, NULL); // 일꾼 끝날 때까지 대기
printf("Main: x = %d\n", x); // 20
}
결과: 일꾼이 x를 20으로 바꾸면, 사장님(Main)도 20을 봅니다. 메모리를 공유하기 때문입니다.
12. 요약 비교표
| 특성 | 프로세스 (Process) | 스레드 (Thread) |
|---|---|---|
| 비유 | 독립된 공장 | 공장 안의 일꾼 |
| 메모리 | 완전 분리 (Code/Data/Heap/Stack) | 공유 (Code/Data/Heap) + 독립 (Stack) |
| 통신 | 어려움 (IPC 필요: 파이프, 소켓) | 쉬움 (메모리 직접 접근) |
| 생성 비용 | 비쌈 (운영체제 오버헤드 큼) | 저렴 (숟가락만 얹으면 됨) |
| 문맥 전환 | 매우 느림 (TLB/Cache Flush) | 빠름 (Register만 교체) |
| 안정성 | 높음 (하나 죽어도 됨) | 낮음 (하나 죽으면 다 죽음) |
| 대표 예시 | 크롬 탭, Nginx 워커 | 파이어폭스 탭, 게임 로직 |
13. 마무리 - 무엇을 선택해야 할까?
"무조건 멀티 스레드가 좋은 거 아냐?" 아닙니다. 상황에 따라 다릅니다.
- 안정성이 최우선이고, 프로세스 하나가 죽어도 서비스는 살아야 한다? -> 멀티 프로세스 (크롬, Nginx).
- 자원 효율과 성능이 최우선이고, 빠른 데이터 공유가 필요하다? -> 멀티 스레드 (Tomcat, 게임 서버).
- 극강의 동시성이 필요하다? -> 그린 스레드/이벤트 루프 (Go, Node.js).
개발자는 이 트레이드오프(Trade-off)를 이해하고 도구를 선택하는 사람입니다. 내 서비스에 어떤 방식을 적용할지는 "얼마나 격리가 필요한가"와 "얼마나 빠른 통신이 필요한가"를 저울질해서 결정합니다.
Process vs Thread: Factory and Workers (Definitive Guide)
1. Prologue: More Threads Made It Slower
When concurrent connections grow, the instinct is to increase the thread pool size. But sometimes, adding more threads makes response times worse.
"More threads should be faster, right? Why is this getting slower?"
This question cuts to the core of the issue. The right question to ask is: "Is this CPU-bound or I/O-bound? And for this workload—would a process or a thread be the right choice?"
Simply knowing that "a thread runs inside a process" isn't enough. The real engineering principles—Memory Sharing, Isolation, and Cost Trade-offs—are what determine the answer in high-concurrency environments.
Today, I'll break this down using the Factory vs Worker analogy that finally made it all click.
2. The Factory Analogy That Made Everything Clear
After months of confusion, I stumbled upon this comparison: Samsung Semiconductor Manufacturing Plant. Suddenly, everything made sense.
Process = "The Entire Factory Building"
- Definition: An executing program with dedicated resources allocated by the OS.
- Context: Imagine Samsung's Pyeongtaek Fab 1—the entire physical facility.
- Key Features:
- Complete Isolation: If Factory A catches fire (crashes), Factory B next door is completely safe. They are separate legal entities with separate infrastructure.
- Resource Ownership: Each factory owns its address space, machines (memory), raw materials (data), and blueprints (code).
- Expensive Communication (IPC): Sending materials from Pyeongtaek to Icheon requires trucks, highways, customs checks. It's slow, bureaucratic, and costly.
Thread = "Workers Inside the Factory"
- Definition: A unit of execution flow within a process.
- Context: Individual workers on the factory floor—Kim, Lee, Park.
- Key Features:
- Shared Resources: Workers share the cafeteria (Heap), bathrooms (Data), and machinery (Code). No duplication.
- Instant Communication: When Kim needs a tool from Lee, he just asks. Direct memory reference. Speed of light.
- Fatal Coupling: If one worker accidentally hits the emergency shutdown button, everyone in the factory stops working. One thread crash = entire process crash.
This analogy saved me. Once I visualized it this way, the memory architecture, context switching costs, and design decisions behind Chrome's multi-process model became crystal clear.
3. Memory Architecture: The Core Distinction
Understanding this memory structure is key to mastering concurrency.
Process Memory Layout
Each process gets 4 independent memory regions from the OS:
- Code (Text Segment): Machine instructions. Read-only.
- Data Segment: Global and static variables.
- Heap: Dynamically allocated memory (
malloc,new). - Stack: Function call history, local variables, return addresses.
Critical Point: These are completely isolated per process. Process A cannot read Process B's memory without OS intervention (IPC).
Thread Memory Layout
Threads are parasitic to processes. They share most resources to maximize efficiency:
- Shared Regions: Code, Data, Heap. (This is why communication is fast).
- Private Regions: Stack and CPU Registers. (Each thread needs its own execution context).
The Million-Dollar Question: "Why does each thread need its own Stack?"
Answer: Because each thread executes functions independently. If they shared a Stack, Thread A calling
functionX()would overwrite Thread B's return address—causing catastrophic corruption.
I struggled with this for weeks. But once I realized the Stack holds function call frames, it became obvious why isolation was necessary.
4. Context Switching: Why Performance Matters
"What is Context Switching?"
It's when the CPU saves the current execution state and loads a different one. But the cost varies wildly depending on whether you're switching processes or threads.
Process Context Switch (Moving the Entire Factory)
- Analogy: Evacuating Factory A, bringing in a new company for Factory B.
- What Happens:
- CPU caches (L1/L2/L3) must be flushed. All that preloaded data? Gone.
- TLB (Translation Lookaside Buffer for virtual memory) must be invalidated.
- Page tables must be swapped.
- Cost: Extremely expensive. Thousands of CPU cycles wasted.
Thread Context Switch (Changing Workers)
- Analogy: One worker clocks out, another clocks in—same factory, same machines.
- What Happens:
- Virtual memory stays the same. Caches remain valid.
- Only CPU Registers and Stack Pointer need updating.
- Cost: Cheap. Minimal overhead.
This is why web servers (Nginx, Apache) prefer threads over processes for handling thousands of concurrent connections. The context switching cost for threads is 10-100x lower.
5. Real-World Case Study: Chrome's Architecture
I used to curse Chrome for eating 8GB of RAM with just 10 tabs open. Then I learned why.
Old Browsers (Single Process Model)
Internet Explorer used to run one process with multiple threads for tabs.
- Problem: If one tab crashed (JavaScript infinite loop, memory leak), the entire browser died. All your tabs—gone.
Chrome's Multi-Process Revolution
Google Chrome made a radical decision in 2008: One process per tab.
- Benefit 1 - Stability: When YouTube crashes, your Gmail tab stays alive. Process isolation saves the day.
- Benefit 2 - Security (Site Isolation): After the 2018 Spectre/Meltdown CPU vulnerabilities, Chrome doubled down on process isolation. Malicious JavaScript on
evil.comcannot read memory fromyourbank.combecause they're in separate processes with separate address spaces. - Cost: Massive memory overhead. Each tab duplicates Code, Data, and Heap. The "Chrome is a RAM hog" meme exists because safety costs memory.
Now when my MacBook fans spin up with 20 Chrome tabs, I don't complain. I understand the trade-off: Isolation and security in exchange for memory.
6. Python in Practice: multiprocessing vs threading
Running these examples makes the difference concrete.
Threading Example (Memory Sharing)
import threading
counter = 0 # Shared global variable
def worker():
global counter
for _ in range(100000):
counter += 1 # Multiple threads touch same memory
threads = []
for _ in range(4):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Result: {counter}") # Expected 400000, got 312451 (Race Condition!)
What Went Wrong: Multiple threads read-modify-write the same memory location without coordination. Classic race condition.
Multiprocessing Example (Memory Isolation)
import multiprocessing
def worker(shared_value):
for _ in range(100000):
with shared_value.get_lock(): # Explicit lock required
shared_value.value += 1
if __name__ == '__main__':
shared_value = multiprocessing.Value('i', 0) # Shared memory segment
processes = []
for _ in range(4):
p = multiprocessing.Process(target=worker, args=(shared_value,))
processes.append(p)
p.start()
for p in processes:
p.join()
print(f"Result: {shared_value.value}") # Exactly 400000
Key Insight:
- Threading: Easy to share data, but dangerous without proper synchronization.
- Multiprocessing: Safe by default (isolated memory), but requires explicit shared memory setup.
The race condition here is subtle but real—and in a production data pipeline, this kind of bug can silently corrupt data before anyone notices.
7. Async/Await: The Third Way
Modern languages introduced a game-changer: Asynchronous programming with event loops.
Event Loop Model (Node.js, Python asyncio)
Here's how you handle thousands of concurrent operations on a single thread:
import asyncio
async def task(name, delay):
print(f"{name} started")
await asyncio.sleep(delay) # Yield control during I/O
print(f"{name} completed")
async def main():
await asyncio.gather(
task("Task1", 2),
task("Task2", 1),
task("Task3", 3)
)
asyncio.run(main())
Output:
Task1 started
Task2 started
Task3 started
Task2 completed # After 1s
Task1 completed # After 2s
Task3 completed # After 3s
Magic: No threads. No context switching. Just cooperative multitasking. When one task waits for I/O (database query, HTTP request), the event loop switches to another task.
When to Use What?
| Workload | Best Choice |
|---|---|
| CPU-bound (image processing, ML training) | Multiprocessing |
| I/O-bound (web APIs, databases, file I/O) | Async/await |
| Mixed or Legacy | Threading (middle ground) |
A common mistake is reaching for threading on CPU-bound work like image processing—threading won't help much there, and multiprocessing is the right tool instead.
8. Coroutines: The Evolution of Lightweight Concurrency
Python asyncio Coroutines
import asyncio
async def fetch_user(user_id):
print(f"Fetching user {user_id}...")
await asyncio.sleep(1) # Simulating database query
return f"User{user_id}"
async def main():
users = await asyncio.gather(
fetch_user(1),
fetch_user(2),
fetch_user(3)
)
print(users)
asyncio.run(main())
Characteristics:
- Lighter than threads (only a few KB per coroutine)
- Explicit yielding with
await - Event loop handles scheduling
Kotlin Coroutines (Real-World Android)
import kotlinx.coroutines.*
suspend fun fetchUser(userId: Int): String {
delay(1000) // Non-blocking delay
return "User$userId"
}
fun main() = runBlocking {
val users = listOf(1, 2, 3).map { userId ->
async { fetchUser(userId) }
}.awaitAll()
println(users) // [User1, User2, User3]
}
Kotlin's Magic:
- Structured Concurrency: Parent coroutine cancellation automatically cancels children.
- suspend functions: Functions that can be paused and resumed (like video game save points).
Kotlin coroutines for network calls in Android apps let code look synchronous while running asynchronously—no callback hell, much easier to follow.
9. Advanced: Java Virtual Threads (Project Loom)
Java 21 introduced a revolutionary feature that changes everything.
The Old Problem (Platform Threads)
// Traditional Java threads (1:1 with OS threads)
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
// Each thread costs ~1MB of memory
// Creating 10,000 threads = 10GB RAM!
}).start();
}
Problem: OS threads are expensive. Java couldn't scale to millions of concurrent tasks.
The New Solution (Virtual Threads)
// Java 21 Virtual Threads (Project Loom)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000_000; i++) { // 10 MILLION
executor.submit(() -> {
// Virtual thread: only a few KB
// OS sees just a handful of platform threads
});
}
}
Magic: Virtual threads are scheduled by the JVM, not the OS. They're M:N threads—millions of virtual threads multiplexed onto a few platform threads.
Impact: Java can now handle the same scale as Go's goroutines without rewriting codebases.
Benchmarks show significant scaling improvements. With platform threads, JVM services start struggling well before tens of thousands of concurrent connections. With virtual threads, the same hardware can handle far higher concurrency—in large-scale traffic environments, this difference is substantial.
10. Rust's Ownership Model: Thread Safety by Design
Here's where Rust blew my mind: compile-time thread safety.
The Problem in C/C++
int* ptr = malloc(sizeof(int));
*ptr = 42;
// Thread 1
*ptr = 100;
// Thread 2 (simultaneously)
*ptr = 200;
free(ptr);
// Thread 3 (simultaneously)
free(ptr); // Double free! Segfault!
Result: Data race, memory corruption, undefined behavior. Good luck debugging.
Rust's Compile-Time Guarantee
use std::thread;
fn main() {
let mut data = vec![1, 2, 3];
thread::spawn(move || {
data.push(4); // Takes ownership
});
println!("{:?}", data); // Compile error!
// Error: value borrowed after move
}
Rust's Rule: Either one mutable reference OR multiple immutable references—never both.
Impact: The compiler prevents data races at compile time. No runtime locks needed for many cases.
This is Rust's core promise: the learning curve is steep, but once it compiles, thread safety is guaranteed at the language level—no race conditions, no segfaults caused by concurrent memory access.
11. Green Threads vs Kernel Threads
Not all threads are created equal. This distinction separates junior from senior engineers.
1) Kernel Threads (Native) - 1:1 Model
- Managed by: OS Kernel.
- Examples: Java's traditional
Thread, C++std::thread, Pythonthreading.Thread. - Pros: True parallelism on multi-core CPUs.
- Cons:
- Expensive creation (~1MB stack per thread).
- Context switch requires kernel mode transition (slow).
2) Green Threads (User-Level) - M:N Model
- Managed by: Language runtime (Go scheduler, JVM virtual threads).
- Examples: Go goroutines, Java 21 Virtual Threads, Erlang processes.
- Pros:
- Extremely cheap (~2KB per goroutine).
- Context switch in user space (10-100x faster).
- Cons: Requires sophisticated runtime scheduler.
Go's Goroutines Example:
func main() {
for i := 0; i < 1_000_000; i++ {
go func() {
// Goroutine: only 2KB
time.Sleep(time.Hour)
}()
}
time.Sleep(time.Hour)
}
This code creates 1 million goroutines and uses only ~2GB of RAM. Try that with OS threads—your machine would explode.
12. IPC (Inter-Process Communication): The Necessary Evil
Processes are isolated. Great for safety. Terrible for communication.
Common IPC Mechanisms
- Pipe: One-way data stream. Parent-child only. (Shell's
|operator). - Socket: Network communication. Works across machines. Slowest but most flexible.
- Shared Memory: Map same RAM region to multiple processes. Fastest but dangerous (manual synchronization required).
- Message Queue: Asynchronous mailbox. Fire-and-forget.
Threads don't need IPC. They just read global variables. Zero overhead.
13. Code Proof (C Language)
Theory is nice. Let's prove it with code.
Process Creation (fork) - Copy Everything
#include <unistd.h>
#include <stdio.h>
int main() {
int x = 10;
pid_t pid = fork(); // Duplicate process
if (pid == 0) {
// Child process
x = 20;
printf("Child: x = %d\n", x); // 20
} else {
// Parent process
sleep(1);
printf("Parent: x = %d\n", x); // 10
}
}
Result: Child modifying x doesn't affect parent. Memory is copied, not shared.
Thread Creation (pthread_create) - Share Memory
#include <pthread.h>
#include <stdio.h>
int x = 10; // Global (shared)
void* worker(void* arg) {
x = 20; // Modifies shared memory
return NULL;
}
int main() {
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
pthread_join(t, NULL);
printf("Main: x = %d\n", x); // 20
}
Result: Worker thread modifies x, main thread sees the change. Memory is shared.
14. Summary Table
| Feature | Process | Thread |
|---|---|---|
| Analogy | Entire factory | Worker inside factory |
| Memory | Isolated (Code/Data/Heap/Stack) | Shared (Code/Data/Heap) + Private (Stack) |
| Communication | Hard (IPC required) | Easy (direct memory access) |
| Creation Cost | Expensive (OS overhead) | Cheap |
| Context Switch | Slow (cache/TLB flush) | Fast (register swap) |
| Stability | Isolated failures | One crash kills all |
| Use Case | Chrome tabs, Nginx workers | Firefox tabs, game engines |