CS 스터디 자료 — 동시성 & 병렬성

같은 시간에 여러 일을 한다는 것

멀티프로세스, 스레드, 코루틴… 흔히 "동시에 여러 작업을 처리하는 방법들"로 뭉뚱그려 배우지만, 이들은 사실 서로 다른 두 가지 문제를 풉니다. 이 자료는 그 차이를 메모리를 공유하는가, 실제로 병렬로 실행되는가라는 두 축으로 정리합니다.

병렬성 — process / isolate / thread 동시성 — coroutine / asyncio 비교 언어: C · C++ · Python · Dart · Rust
0

두 개의 축: 동시성과 병렬성

동시성(concurrency)은 여러 작업을 "번갈아 가며 처리해서 마치 동시에 진행되는 것처럼 보이게" 만드는 것이고, 병렬성(parallelism)은 여러 작업을 물리적으로 다른 CPU 코어에서 "실제로 동시에" 실행하는 것입니다. 둘은 다른 질문에 대한 답입니다. 동시성은 "일의 구조를 어떻게 짤 것인가"의 문제이고, 병렬성은 "하드웨어 자원을 어떻게 나눠 쓸 것인가"의 문제입니다.

이 관점에서 보면 이 자료가 다루는 네 가지 기법은 셋과 하나로 갈립니다.

메모리를 공유한다
메모리를 공유하지 않는다
병렬
(멀티코어)
Multi-thread같은 프로세스 안, 힙을 공유하며 각 코어에서 동시 실행. GIL이 없을 때 한정.
Multi-process · Dart isolate주소공간이 완전히 분리된 채 각 코어에서 동시 실행. 통신은 IPC/메시지로만.
동시성만
(단일 코어)
Coroutine / asyncio하나의 스레드 위에서 협력적으로 번갈아 실행. I/O 대기 시간을 다른 작업으로 채움.
해당 없음

즉 앞의 세 기법(멀티프로세스, Dart isolate, GIL-free 멀티스레드)은 "실제로 동시에 CPU를 쓴다"는 공통점이 있고, 마지막 코루틴/asyncio는 "동시에 도는 것처럼 보이지만 실제로는 한 스레드가 번갈아 처리한다"는 점에서 근본적으로 다릅니다. CPU 연산 자체가 겹쳐 도는 게 아니라, I/O를 기다리는 시간에 다른 작업을 끼워 넣는 것이 핵심입니다.

1

Multi-process병렬성

운영체제가 만들어주는 가장 기본적인 실행 단위입니다. 각 프로세스는 자신만의 독립된 주소공간(코드, 힙, 스택)을 가지고 있어서 한 프로세스가 다른 프로세스의 메모리를 직접 들여다볼 수 없습니다. OS 스케줄러가 여러 프로세스를 서로 다른 코어에 배치하면 진짜 병렬 실행이 일어납니다.

실행 타임라인 — Core 1 / Core 2
Core 1
Core 2

프로세스 A, B는 서로 다른 주소공간을 가진 채 서로 다른 코어에서 동시에 실행됩니다.

장점과 비용

언어별 지원

언어지원 방식비고
C 지원fork(), exec() (POSIX) OS 시스템 콜을 직접 사용. Windows는 CreateProcess로 별도 API.
C++ 지원 — 표준 라이브러리엔 없고 OS API 직접 사용 C++ 표준 자체엔 프로세스 생성 API가 없어 POSIX fork나 Windows API에 의존.
Python 지원multiprocessing GIL 문제를 우회해 진짜 병렬 CPU 연산이 필요할 때 가장 널리 쓰는 표준 해법.
Dart 지원dart:ioProcess.start() 완전히 별도의 OS 프로세스를 실행하는 것으로, 아래의 isolate와는 다른 개념임에 주의.
Rust 지원std::process::Command 표준 라이브러리에 포함.
더 읽어보기
2

Dart Isolate병렬성

Isolate는 이름 그대로 "격리된" 실행 단위입니다. 겉보기엔 스레드처럼 가볍게 여러 개를 띄울 수 있지만, 본질은 스레드가 아니라 경량화된 프로세스에 더 가깝습니다. 스레드의 정의적 특징은 같은 힙을 공유한다는 것인데, isolate는 정반대로 각자 독립된 힙을 가지고 아무 것도 공유하지 않습니다(share-nothing). 통신은 오직 SendPort/ReceivePort를 통한 메시지 패싱으로만 이루어지며, 이 점에서 Erlang/Elixir의 프로세스나 Actor 모델과 철학적으로 가깝습니다.

주의 — 각 isolate는 내부적으로 자신만의 이벤트 루프를 돌립니다. 즉 Dart isolate는 "1번(프로세스처럼 격리) + 4번(내부는 이벤트 루프 기반)"이 결합된 하이브리드 구조이지, 단순히 스레드의 상위 버전이 아닙니다.
실행 타임라인 — Isolate A / Isolate B
Isolate A
Isolate B

각 isolate는 독립된 힙과 자체 이벤트 루프를 가진 채 다른 코어에서 동시에 실행됩니다. 서로의 메모리는 볼 수 없고 메시지로만 대화합니다.

왜 이런 설계를 택했는가

공유 메모리 스레드는 빠르지만 race condition과 lock 관리라는 대가를 치릅니다. Dart는 (특히 Flutter의 UI 스레드가 절대 멈추면 안 된다는 요구 때문에) 처음부터 공유 상태 동시성을 언어 차원에서 배제하고, 무거운 계산은 별도 isolate로 보내 메인 isolate(UI)를 절대 막지 않는 쪽을 택했습니다.

언어별 지원

언어지원 방식비고
C해당 없음share-nothing 격리 실행 단위라는 개념 자체가 언어에 없음. 프로세스로 유사하게 흉내 낼 수는 있음.
C++해당 없음동일. 언어 고유 개념 없음.
Python해당 없음직접 대응 개념은 없고, multiprocessing이 "메모리 격리 + 병렬"이라는 목표는 유사하게 달성.
Dart고유 지원dart:isolateDart/Flutter의 핵심 동시성·병렬성 모델.
Rust해당 없음대신 ownership 모델이 공유 메모리 스레드를 안전하게 만드는 다른 접근을 취함(3번 참고).

참고: 철학적으로 가장 가까운 것은 Erlang/Elixir의 프로세스, Go의 goroutine + channel 조합입니다. 다만 이들은 이번 비교 대상 언어(C, C++, Python, Dart, Rust)에는 포함되지 않습니다.

더 읽어보기
3

Multi-thread병렬성

스레드는 같은 프로세스 안에서 힙과 전역 상태를 공유하는 실행 단위입니다. 프로세스보다 훨씬 가볍게 생성할 수 있고, 데이터를 주고받을 때도 메모리를 그냥 같이 보면 되니 IPC보다 빠릅니다. 대신 그 대가로 여러 스레드가 같은 데이터를 동시에 건드리면 race condition이 생길 수 있고, 락을 잘못 쓰면 deadlock에 빠집니다.

실행 타임라인 — Thread A / Thread B (같은 프로세스, 공유 힙)
Thread A
Thread B

두 스레드가 같은 힙을 공유하며 서로 다른 코어에서 동시에 실행됩니다. 공유 데이터에 접근할 때는 동기화(lock)가 필요합니다.

Python의 GIL, 그리고 GIL-free

CPython은 오랫동안 GIL(Global Interpreter Lock) 때문에 한 순간에 하나의 스레드만 Python 바이트코드를 실행할 수 있었습니다. 이는 버그가 아니라 설계 결정이었습니다 — CPython의 레퍼런스 카운팅 기반 메모리 관리를 스레드 세이프하게 만들기 위한 트레이드오프였습니다. 그 결과 CPU-bound 작업에서는 Python 스레드를 아무리 늘려도 코어를 하나만 쓰는 것과 큰 차이가 없었습니다.

Python 3.13부터 GIL을 제거한 free-threaded 빌드(PEP 703)가 도입되었지만, 이는 아직 실험적인 opt-in 빌드입니다. 기본 배포판은 여전히 GIL이 켜져 있고, 3.14에서 "공식 지원" 단계로 올라섰을 뿐 기본값 전환은 한참 뒤의 일입니다. 즉 "Python 스레드는 이제 항상 병렬이다"라고 단정하면 안 됩니다.

python — GIL-free 빌드 확인
python -VV
# ... "free-threading build" 문구가 있는지 확인
import sys
sys._is_gil_enabled()  # False면 이 인터프리터에선 GIL이 꺼져 있음

Rust의 fearless concurrency

Rust는 공유 메모리 스레드의 위험을 런타임이 아니라 컴파일 타임에 막습니다. ownership과 borrow checker가 "두 스레드가 같은 데이터를 동시에 변경 가능한 상태로 접근"하는 코드를 아예 컴파일되지 않게 만듭니다. 그래서 Rust는 이 모델을 "fearless concurrency"라고 부릅니다 — 안전성을 프로그래머의 주의력이 아니라 타입 시스템이 보장한다는 점에서 C/C++/Python과 접근 자체가 다릅니다.

언어별 지원

언어지원 방식비고
C지원pthread (POSIX Threads)표준 C 언어 자체엔 없고 POSIX 라이브러리에 의존. C11부터 <threads.h>가 표준에 포함되었으나 실사용은 pthread가 압도적.
C++지원std::thread (C++11~)표준 라이브러리에 포함, mutex/condition_variable 등 동기화 도구도 함께 제공.
Python부분 지원threading기본 빌드는 GIL 존재(진짜 병렬 아님). 3.13+ free-threaded 빌드에서만 진짜 병렬, 아직 실험 단계.
Dart직접 노출 안 함OS 스레드를 언어 차원에서 감추고 isolate로 추상화. 공유 메모리 스레딩 모델 자체를 의도적으로 배제.
Rust지원std::threadownership/borrow checker가 컴파일 타임에 data race를 방지("fearless concurrency").
더 읽어보기
4

Coroutine · asyncio동시성

여기서부터는 앞의 세 기법과 완전히 다른 게임입니다. 코루틴/asyncio는 기본적으로 하나의 스레드 위에서 동작합니다. "동시에 실행"되는 게 아니라, 한 작업이 I/O(네트워크 응답, 파일 읽기, DB 쿼리 등)를 기다리는 동안 그 틈에 다른 작업을 실행시켜 번갈아 진행하는 방식입니다. CPU 연산이 겹쳐 도는 것이 전혀 아니라는 점이 핵심입니다.

실행 타임라인 — 하나의 스레드가 Task A / Task B를 번갈아 처리
Thread

Task A가 I/O를 기다리는 동안(빈 구간) 스케줄러가 Task B를 실행시킵니다. 단일 스레드이므로 두 색이 동시에 나타나는 구간이 없습니다.

event loop와 job queue

실행 구조는 대략 이렇습니다: 작업들이 큐에 쌓이고, 이벤트 루프가 큐에서 하나씩 꺼내 실행하다가 await 지점을 만나면 제어권을 루프에 돌려주고, 루프는 다음 작업을 꺼내 실행합니다. async/await는 이 상태 기계(state machine)를 사람이 읽기 쉬운 문법으로 감싼 syntactic sugar입니다.

이 모델이 강력한 이유는 context switching 비용이 거의 0에 가깝기 때문입니다. OS 스레드 전환은 커널 개입이 필요해 비싸지만, 코루틴 전환은 그냥 함수 호출 수준이라 수만 개의 동시 연결(예: 채팅 서버)도 무리 없이 처리할 수 있습니다.

흔한 함정 — asyncio/코루틴은 I/O-bound 작업(대기 시간이 많은 작업)에는 강력하지만, CPU-bound 작업(순수 연산)에는 아무 도움이 안 됩니다. 한 코루틴이 CPU를 계속 붙잡고 있으면 다른 코루틴에게 제어권이 넘어갈 기회 자체가 없어서, 오히려 전체가 멈춘 것처럼 보일 수 있습니다.

언어별 지원

언어지원 방식비고
C표준 없음언어 차원의 코루틴이 없어 libuv, libevent 같은 별도 라이브러리로 이벤트 루프를 직접 구현.
C++부분 지원 — C++20 co_await/co_yield/co_return언어가 제공하는 건 코루틴을 만들기 위한 저수준 빌딩 블록뿐이고, 실행기(executor/scheduler) 자체는 표준에 없음. 실사용엔 Boost.Asio 등 라이브러리가 필수 — Rust와 같은 함정.
Python지원asyncio이벤트 루프, async def/await 문법, 실행기까지 표준 라이브러리에 모두 포함.
Dart지원async/await + Future모든 isolate는 내부에 자신만의 이벤트 루프를 가지며 그 위에서 async/await가 동작.
Rust부분 지원async/await 문법은 표준C++20과 마찬가지로 실행기가 표준 라이브러리에 없음. tokio, async-std 같은 외부 크레이트가 사실상 필수.
더 읽어보기
5

종합 비교

기법별 특성

기법메모리 모델병렬 실행생성 비용통신 방식적합한 작업
Multi-process
병렬성
완전 격리O높음IPC (파이프·소켓·공유메모리)CPU-bound, 강한 격리·안정성 필요
Dart isolate
병렬성
격리 (share-nothing)O중간메시지 패싱 (Port)UI 스레드를 막지 않는 무거운 연산
Multi-thread
병렬성
공유 (같은 힙)O (GIL 있으면 X)낮음공유 메모리 + lockCPU-bound 병렬 처리, 세밀한 제어
Coroutine / asyncio
동시성
공유 (단일 스레드)X매우 낮음이벤트 루프 (콜백/await)I/O-bound, 수많은 동시 연결

언어별 지원 한눈에 보기

언어Multi-processDart isolateMulti-threadCoroutine/asyncio
CO fork/execO pthread표준 없음
C++O OS APIO std::thread부분 co_await + 별도 실행기
PythonO multiprocessing부분 GIL / 3.13+ 실험적 free-threadedO asyncio
DartO Process.startO 고유 지원비노출 isolate로 추상화O async/await
RustO std::processO std::thread + 컴파일타임 안전부분 async/await + 별도 실행기(tokio)

O = 표준/기본 지원, 부분 = 문법은 있으나 핵심 요소가 실험적이거나 외부 의존, — = 해당 개념 자체가 없음

+

함께 알아두면 좋은 개념

위 네 가지 기법을 실전에서 고를 때, 그리고 더 깊이 이해할 때 도움이 되는 배경 지식입니다.

01 CPU-bound vs I/O-bound

실전에서 가장 중요한 판단 기준입니다. 연산 자체가 오래 걸리는 작업(이미지 처리, 암호화, 수치 계산)은 CPU-bound이므로 병렬성(process/thread/isolate)이 필요하고, 대기 시간이 대부분인 작업(네트워크 요청, DB 쿼리, 파일 I/O)은 I/O-bound이므로 동시성(coroutine/asyncio)만으로 충분하거나 오히려 더 효율적입니다.

02 Race condition과 deadlock

메모리를 공유하는 스레드는 두 작업이 같은 데이터를 동시에 건드리면 결과가 실행 순서에 따라 달라지는 race condition에 취약합니다. 이를 lock으로 막으려다 서로가 서로의 lock을 기다리면 deadlock이 됩니다. 프로세스와 isolate가 메시지 패싱을 고집하는 이유가 바로 이 문제를 구조적으로 없애기 위해서입니다.

03 Context switching 비용의 스펙트럼

process(커널 개입, 무거움) > OS thread(커널 개입, 중간) > coroutine(사용자 공간, 거의 무료) 순으로 전환 비용이 줄어듭니다. asyncio 서버가 수만 개의 동시 연결을 별문제 없이 처리하는 이유가 여기 있습니다.

04 Amdahl's Law

프로그램을 아무리 병렬화해도 직렬로 실행해야 하는 구간이 남아있는 한 전체 속도 향상에는 한계가 있습니다. "코어를 늘리면 무조건 그만큼 빨라진다"는 흔한 오해를 짚어주는 법칙입니다.

05 GIL은 왜 존재했는가

CPython의 레퍼런스 카운팅 기반 가비지 컬렉션을 스레드 세이프하게 만들기 위한 설계 트레이드오프였습니다. 단순한 "나쁜 제약"이 아니라 당시 상황에서의 합리적 선택이었다는 배경을 알면, free-threading이 왜 이렇게 오래 걸렸는지도 이해가 됩니다.

06 컴파일 타임 안전성이라는 다른 접근

Rust의 ownership/borrow checker는 "안전성을 프로그래머의 주의력이 아니라 타입 시스템으로 강제한다"는 점에서 C/C++/Python과 근본적으로 다른 언어 설계 철학을 보여줍니다. 동시성 기법을 비교할 때 언어 설계 관점에서 함께 짚어보면 좋은 지점입니다.