시작하기 전에
(출처: https://parang0309.tistory.com/30)
- 동시성 관련 개념들을 수박겉핥기 수준으로 살펴보겠습니다.
- 여러 언어들의 동시성 기법들을 전체적으로 조망합니다.
- 예제 코드는 기초적인 예제 중심이고, 이해하기 쉽도록 짧은 코드를 이용했습니다.
7 Concurrency Models (in 7 Weeks)
- Thread & Lock
- Functional Programming
- Clojure Way (Separating Identity from state)
- Actor
- Communicating Sequential Processes
- Channel
- Data Parallelism
- Lambda Architecture
이 중에서 서비스 개발 현업에서 사용되고 있는 개념이나 기술에는 Thread & Lock, Functional, Actor(개념만), Data Parallelism, Channel 등이 있음
Lambda Architecture는 big data 분야에서 널리 쓰이지만 프로그래밍 레벨로 들어온 것이 아니라서(아키텍쳐 레벨임) 여기서 다루지 않음
Concurrency vs Parallelism
| Concurrency | Parallelism | |
|---|---|---|
| 한글 용어 | 동시성 | 병렬성 |
| 범위 | 가상적/개념적 | 물리적 |
| 관점 | 설계 | 구현/하드웨어 |
| 의미 | 겹치는 시간 동안에 여러 태스크가 동시에 실행되는 것 | 여러 태스크가 다중 컴퓨팅 자원을 가지고 같은 시간에 실행되는 것 |
![]() (출처: https://freecontent.manning.com/concurrency-vs-parallelism/) |
![]() (출처: https://freecontent.manning.com/concurrency-vs-parallelism/) |
|
| 구어체로 표현하자면 | 한 번에 많은 일을 | 많은 일을 동시에 |
| 단일 코어에서 | 동시성을 가질 수 있음 | 병렬성을 가질 수 없음 |
Thread & Locks
- 가장 전통적인 동시성 방법
Java의 Thread 예제
public class SimpleThreadExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> System.out.println("Hello from Thread 1"));
Thread thread2 = new Thread(() -> System.out.println("Hello from Thread 2"));
thread1.start();
thread2.start();
try {
thread1.join(); // 첫 번째 쓰레드가 완료될 때까지 대기
thread2.join(); // 두 번째 쓰레드가 완료될 때까지 대기
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Hello from Thread 1
Hello from Thread 2
(순서 뒤바뀔 수 있음)
C & POSIX Thread 예제
#include <stdio.h>
#include <pthread.h>
void* printHello(void* id) {
printf("Hello from Thread %ld\n", (long)id);
return NULL;
}
int main() {
pthread_t threads[2];
for (long i = 0; i < 2; i++) {
pthread_create(&threads[i], NULL, printHello, (void*)i);
}
for (int i = 0; i < 2; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
Hello from Thread 0
Hello from Thread 1
(순서 뒤바뀔 수 있음)
C++11의 Thread 예제
#include <iostream>
#include <thread>
void printHello(int id) {
std::cout << "Hello from Thread " << id << std::endl;
}
int main() {
std::thread t1(printHello, 1);
std::thread t2(printHello, 2);
t1.join();
t2.join();
return 0;
}
Hello from Thread 2
Hello from Thread 1
(순서 뒤바뀔 수 있음)
Thread의 공통적인 특징
- thread를 생성하거나 thread 객체에 대해 start를 하면 실행됨
- main thread가 worker thread를 기다리기 위해 join을 하고 있음(wait)
- join을 안 하면 main thread가 worker thread를 기다리지 않고 먼저 자기 할 일만 하고 종료할 수도 있음
- 쓰레드는 동시에 실행되고 메모리를 공유하니 동시에 메모리 데이터를 변경하면 데이터의 정합성이 깨짐 -> 동기화(synchronization)가 필요함
Thread의 장단점
- 장점
- 오래된 기술이라서 소프트웨어 개발자라면 누구나 다 아는 개념
- 메모리 공유
- 단점
- 쓰레드 하나에 fault가 발생하면 프로그램 전체에 영향
- 데이터 교환하려면 동기화 기본연산 필요
- race condition에 주의해야 함
Race Condition

(출처: https://comic.browserling.com/53)
Thread와 관련하여 알아두면 좋은 상식: detach
- POSIX thread에서는 detach의 개념이 있었음
- 자원 회수를 누가/언제 할 것이냐에 대한 명시적 선언
- 일반적으로는 join으로 인해 기다렸다가 자원을 회수하는데, detach를 하면 백그라운드에서 자원이 회수됨
- Java는 GC가 있어서 명시적 선언이 불필요함
- 자원 회수를 누가/언제 할 것이냐에 대한 명시적 선언
Thread와 관련하여 알아두면 좋은 상식: Thread Local Data
- thread는 기본적으로 서로 메모리를 공유하는데, 각자만의 데이터가 필요하기도 함
- POSIX Thread에서는 "thread specific data"(tsd)라고 불렀음
- Java에서는 ThreadLocal 클래스가 있음
동기화 기본연산(Synchronization Primitives)
| 기본연산 \ 언어 | Java | Javascript | C++ | Python |
|---|---|---|---|---|
| Mutex(lock) | O (ReentrantLock) |
? (Atomics) |
O (C++20) |
O |
| Semaphore | O | X | O (C++20) |
O |
| Critical Section | O (synchronized) |
X | X | X |
| Conditional Variable | O | X | O | O |
| Read-Write Lock | O | X | O | X |
| (Cyclic) Barrier | O | X | O (C++20) |
O |
| Countdown Latch | O | X | O (C++20) |
X |
| Phaser | O | X | X | X |
| Exchanger | O | X | X | X |
기본연산의 주요 개념
- (cyclic) barrier
- 쓰레드 n개가 모일 때까지 기다렸다가 실행을 재개하는 것
- 어떤 지점에 쓰레드들을 모을 때 사용
- countdown latch
- n개를 카운트다운해서 0에 되면 쓰레드의 실행을 재개하는 것
- 특정 갯수의 이벤트가 발생하거나 작업이 완료되기를 기다릴 때 사용
- phaser
- phaser는 단계별로 모두 준비가 될 때까지 기다렸다가 다음 단계로 넘어가는 추상화된 barrier 또는 countdown latch라고 할 수 있음
- exchanger
- 2개의 쓰레드가 서로 데이터를 교환하는 기능
- producer/consumer 모델
의문점
- Javascript는 왜 동기화 기본연산이 거의 없지?
- single thread 기반의 event loop 구조라서 "Thread & Lock" 모델에 관심이 없음
- Java와 다른 언어는 왜 이리 다른 양상을 보이지?
- Java는 지금까지도, 그리고 앞으로도 thread에 진심임
- C++나 Python, Javascript는 동시성 프로그래밍에서 못쓸 물건인가?
- thread & lock 모델보다는 다른 동시성 모델을 추구하고 있음
동시성 기법의 과거와 미래
과거: Thread의 종류와 역사
- native thread / kernel thread
- OS가 제공하는 thread
- 비표준화
- POSIX thread
- 표준화된 API
- 내부적으로는 native thread
- green thread
- native thread는 context switching 비용이 상당해서 이걸 응용이나 런타임 레벨에서 해결하기 위해 green thread 등장
- single core 시절에 등장했는데 multi core 시대에 진입하면서 쓸모가 없어짐
- Java
- 초기에 잠시 green thread를 이용하여 이식성에서는 이익을 얻었지만 multi core 환경에서의 성능 상 이익을 얻지 못함
- 현재는 native thread를 이용함
- Java 7,8 시절에 thread synchronization primitive들을 충분히 추상화해서 기능을 갖춰놨음
- 이제는 좀 더 경량화를 통해 성능을 추구하고 있음
미래
- 경량 쓰레드(light-weight thread)라는 일반적인 개념으로 fiber가 있음 (언어 공통)
- Java는 Project Loom에서 "Virtual Thread"라는 light-weight thread를 준비해 왔음
- ForkJoinPool를 이용해서 관리하고 스케쥴함
- 23년 가을에 출시될 Java 21에 포함됨
- C++은 C++20에서 동기화 기본연산 몇 가지가 추가됨
- C++은 coroutine에 주력할 예정?
- 아직 MS VC++이나 GCC에서 full spec을 지원하지 않고 있음
- Javascript나 Python은 예전에 이미 coroutine 도입을 마쳤음
Virtual Thread
특징
- 경량화
- Java 런타임에서 관리하여 생성과 스케쥴링 비용을 최적화
- 수백만 개의 virtual thread 생성 가능
- blocking IO에 (상대적으로) 유리함
- IO block되면 parking해둠
- 효율성
- 메모리 사용량 작음
- 기존 쓰레드 프로그래밍 스타일과 유사하고, ForkJoinPool에 의존하므로 추가 개념을 이해할 필요가 없음
예제
public class VirtualThreadExample {
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
Thread.startVirtualThread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
- 이 예제를 실행하면 완료까지 얼마나 오래 걸릴까?
Coroutine
Coroutine에 앞서 Promise & Future
- 개념 정리
- promise는 채권/채무 계약
- future는 채권액
- success callback은 채권액을 돌려받은 다음에 사용할 투자 계획
- 프로그래밍 언어에서
- 대부분 promise와 future를 혼재해서 사용함
- Javascript
- promise는 객체로 존재하는데 future는 promise가 callback에 넘겨주는 변수로서만 존재함
- C++은 분리된 개념 제공
- Java는 promise 개념이 없고 Future 인터페이스의 구현체에서 처리
- ListenableFuture, CompletableFuture
Javascript의 Promise & Future 예제
let myPromise = new Promise(function(resolve, reject) {
try {
...
resolve("완료");
} catch (error) {
reject(new Error("에러 발생!"));
}
});
myPromise.then((result) => {
console.log(result);
}).catch((error) => {
console.error(error);
});
C++의 Promise & Future 예제
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread( [&p]{ p.set_value_at_thread_exit(9); }).detach();
this_thread::sleep_for(chrono::seconds(1));
try {
f.get();
} catch (const std:exception& e) {
...
}
Java의 Promise & Future 예제
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "future example");
future.get();
Coroutine에 앞서 Generator
- Python과 Javascript의 역사에서 generator가 더 먼저 등장함
- generator는 coroutine의 기반을 먼저 닦았으나 용도가 제한적임
- 비동기적으로 데이터를 생성해내는 기능만
Python의 Generator 예제
def test_generator():
yield 1
yield 2
yield 3
gen = test_generator()
next(gen)
next(gen)
next(gen)
next(gen) # exception
- "generator 기반의 coroutine"이 잠시 사용됨
- 그저 generator 문법의 yield를 사용했다는 의미
Python의 초기 Coroutine 예제
def simple_coroutine():
print('Coroutine started')
x = yield
print('Coroutine received:', x)
coro = simple_coroutine()
next(coro)
coro.send(42)
- 초기 coroutine은 호출자와 코루틴 사이에서 들락날락하는 기능을 중점적으로 제공했음
- 그러나 비동기/동시성 프로그래밍의 목적을 생각해본다면
- 저수준의 "들락날락" 기능이 아니라
- 백그라운드에서(비동기적으로) 잘 실행되고 그 결과를 받아오는 것이 더 중요함
- 추상화되면 더 바람직함
Coroutine이란
![]()
- 특성
중단/재개-> suspend(yield) / resume- 스택 독립성
- 일반 함수(서브루틴)은 스택 안에 프레임 할당, 코루틴은 동적으로 스택 바깥에 할당
- 상호 운용성
- non-blocking
- 상태 보존
재진입가능
- 장점
- promise와 generator를 계승/발전시킨 비동기 프로그래밍 기능
- 프로그래밍 측면에서 async/await 문법은 너무 편리함 (coroutine의 필수 요소는 아님)
- coroutine은 IO 작업에 더 적합함
- callback hell 해소
Coroutine의 내부 구현 (저수준) - resume 기능 관점에서
- C/C++로 coroutine을 직접 구현해보면서 작동 원리를 이해
- setjmp()와 longjmp() system call을 이용할 수 있음
- setjmp()를 호출하면 해당 실행 위치를 기억함. 처음 호출하여 저장될 때는 0을 반환함
- longjmp()를 호출하면 기억된 실행 위치로 jump하여 프로그램의 실행을 이어나감. longjmp()의 매개변수로 지정한 값이 있으면 setjmp()의 반환값으로 사용됨
#include <stdio.h>
#include <setjmp.h>
jmp_buf main_routine_buf, coroutine_buf;
void coroutine() {
printf("Coroutine start\n");
if (setjmp(coroutine_buf) == 0) {
longjmp(main_routine_buf, 1);
}
printf("Coroutine end\n");
}
int main() {
printf("Main start\n");
if (setjmp(main_routine_buf) == 0) {
coroutine();
} else {
printf("Back in main\n");
longjmp(coroutine_buf, 1);
}
printf("Main end\n");
return 0;
}
Main start
Coroutine start
Back in main
Coroutine end
Main end
현대적인 async/await 스타일의 Coroutine

(출처: https://www.reddit.com/r/programminghumor/comments/j6cvwj/async_await/)
Javascript의 Coroutine 예제
async function fetchData(url) {
let response = await fetch(url);
let data = await response.json();
console.log(data);
}
fetchData("https://api.example.com/data");
- Javascript는 single thread event loop 위에서 실행되다보니 어지간한 IO는 non-blocking / async IO로 개발되어 있음
- blocking IO를 크게 신경쓸 필요가 없음
- synchronous IO는 아예 없나?
Python의 Coroutine 예제
import asyncio
async def say_hello(delay):
await asyncio.sleep(delay)
print(f"Hello after {delay} seconds!")
async def main():
task1 = asyncio.create_task(say_hello(1))
task2 = asyncio.create_task(say_hello(2))
await task1
await task2
asyncio.run(main())
- Python은 blocking / synchronous IO 기반으로 개발되고 발전해왔음
- coroutine을 도입할 때 많은 어려움이 발생함
- asyncio 패키지가 많은 부분을 해결해 줌. FastAPI도 큰 도움이 됨
- async/await를 달다보면 main()부터 시작해서 어지간한 함수는 async로 선언됨
- blocking IO를 담당하는 함수만 별도로 처리해주면 됨
- asyncio의 스케쥴링 분리 기능을 활용
- event loop를 유지하면서 특정 작업만 별도 쓰레드로 분리해서 처리하는 방식
import asyncio
import time
def blocking_io():
time.sleep(1)
return "I/O operation completed."
async def main():
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, blocking_io)
print(result)
asyncio.run(main())
Python의 Coroutine Task
- Python에서는 coroutine에 의해서 수행되는 작업을 "task"라는 추상화된 개념으로 지칭함
- 대기: gather, wait
- 취소/보호: cancel, shield
- 통신: queue
- 동기화 기본연산: lock, semaphore
import asyncio
async def worker(name, seconds):
print(f"Worker {name} started, will sleep for {seconds} seconds")
await asyncio.sleep(seconds)
print(f"Worker {name} finished after {seconds} seconds")
async def main():
tasks = [
worker("A", 5),
worker("B", 3),
worker("C", 2),
worker("D", 4)
]
await asyncio.gather(*tasks)
asyncio.run(main())
C++20의 Coroutine 예제 (기본)
#include <iostream>
#include <coroutine>
using namespace std;
struct task {
struct promise_type {
task get_return_object() { return {}; }
suspend_never initial_suspend() { return {}; }
suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
task simple_coroutine() {
cout << "inside coroutine!" << endl;
co_return;
cout << "end of coroutine" << endl;
}
int main() {
cout << "main start" << endl;
simple_coroutine();
cout << "main end" << endl;
return 0;
}
main start
inside coroutine!
main end
task 구조체와 promise_type 구조체는 사실 상 추상 클래스에 가까워서 필요한 기능을 추가 구현해야 할 수도 있음
C++20의 Coroutine 예제 (resume)
- coroutine을 재개하는 기능을 추가하게 되면 promise를 다루기 위해 더 많은 선언과 구현이 필요해져서 복잡해짐
#include <iostream>
#include <coroutine>
using namespace std;
struct task {
struct promise_type {
task get_return_object() { return task(handle_type::from_promise(*this)); }
suspend_always initial_suspend() { return {}; }
suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
using handle_type = coroutine_handle<promise_type>;
handle_type coro;
task(handle_type h) : coro(h) {}
~task() { if (coro) coro.destroy(); }
void resume() { if (!coro.done()) coro.resume(); }
};
task simple_coroutine() {
cout << "Before suspension" << endl;
co_await suspend_always{};
cout << "After suspension" << endl;
co_return;
}
int main() {
auto t = simple_coroutine();
cout << "Coroutine created but not yet resumed." << endl;
t.resume();
cout << "Coroutine resumed and completed." << endl;
return 0;
}
Coroutine created but not yet resumed.
Before suspension
Coroutine resumed and completed.

(출처: https://www.reddit.com/r/ProgrammerHumor/comments/10s12uf/c_meetup/)
C++의 현실적인 대안 예제
- promise & future 스타일로 작성하는 게 그나마 깔끔함
- async를 이용해서 promise보다 coroutine에 더 가까운 문법으로 작성 가능함
#include <iostream>
#include <future>
int main() {
auto future1 = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(1));
return 42;
});
auto future2 = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 99;
});
int result1 = future1.get();
int result2 = future2.get();
std::cout << "Result from task1: " << result1 << std::endl;
std::cout << "Result from task2: " << result2 << std::endl;
return 0;
}
Result from task1: 42
Result from task2: 99
Coroutine의 통신 방법
- 데이터 교환이나 통신은 어떻게 하지?
- 기본적인 방법
- 전역 변수를 사용해서 coroutine 내부로 전달할 수 있음
- 매개 변수를 이용해서 ...
- 리턴값을 사용해서 외부로 전달할 수 있음
- coroutine 사이에서 통신하려면?
채널이나 메시지큐를 이용
- 기본적인 방법
Channel
- 채널이란?
- 7 Concurrency Models (in 7 Weeks) 중 Communicating Sequential Processes (CSP)라는 모델에서 제안된 전달 메커니즘
- 특징
- synchronous
- MQ는 일반적으로 asynchronous
- first-class citizen
- 채널 자체를 언어 내부에서 클래스로 인식함
- 채널 인스턴스를 전달하거나 배열로 만들 수 있음
- Go lang
- synchronous
Go lang의 Channel 예제
package main
import (
"fmt"
"time"
)
func sender(ch chan int) {
for i := 0; i < 5; i++ {
fmt.Println("Sending:", i)
ch <- i // 값을 채널에 송신
time.Sleep(time.Second)
}
close(ch)
}
func receiver(ch chan int) {
for {
value, ok := <-ch // 채널로부터 값 수신
if !ok {
fmt.Println("Channel closed!")
break
}
fmt.Println("Received:", value)
}
}
func main() {
ch := make(chan int)
go sender(ch)
go receiver(ch)
time.Sleep(6 * time.Second)
}
Sending: 0
Received: 0
Sending: 1
Received: 1
Sending: 2
Received: 2
Sending: 3
Received: 3
Sending: 4
Received: 4
Channel closed!
Wrap-up
추상화 단계
- thread < promise & future < coroutine < actor
- promise는 (callback을 객체화해서 callback hell을 벗어나려 했으나 여전히) callback 스타일
- coroutine은 promise를 async/await 형태로 다룸 (callback hell 해소)
- actor는 상태를 공유하지 않고, fiber 수준으로 경량화하고, 메시지 전송과 메일박스(패턴 매칭)을 통해 통신을 함
- 고도로 추상화되었으나 특정 언어(Erlang)를 제외하고는 현업에서는 잘 쓰이지 않음
- 미래에는? (십수 년 전에도 actor가 주목받았는데?)
요즘 기술 트렌드
- Javascript의 승리?
- single threaded event loop 구조를 기반으로 하고 있음
- Java의 반격
- reactive stream
- 데이터 관점에서 프로그래밍
- lightweight threading
- reactive stream
- 각종 언어들
- coroutine 유행 -> task라는 개념으로 추상화 진행
- 문법이 async/await로 통일되고 있는 중
- 내부 구현은 제각각이지만 겉보기에는 async/await 스타일의 coroutine으로 대동단결하고 있는 것으로 보임
- Go lang의 channel같은 추상화된 통신 수단이 아직 널리 보급되지 않았음
- 각개 약진 중
흑묘백묘론

(출처: https://namu.wiki/w/암컷이든 수컷이든 맛있으면 그만 아닐까%3F)
자신이 사용하는 언어에서 제공하는 적당한 수준으로 추상화된 기술을 적절히 구사하면 됨

