코루틴
더 많은 작업
- Coroutine; 코루틴
- 실행을 멈췄다가 멈춘 자리에서 다시 이어 갈 수 있는 서브루틴
코루틴(coroutine)은 실행 도중 제어를 넘기고(중단, suspend) 나중에 바로 그 자리에서 실행을 이어 가는(재개, resume) 루틴이다. 보통의 서브루틴은 맨 앞에서 시작해 한 번 반환하면 끝나지만, 코루틴은 여러 번 멈추고 다시 들어올 수 있으며 멈춰 있는 동안 지역 변수와 실행 위치를 그대로 간직한다. 이름은 "함께"를 뜻하는 co-와 routine을 합친 말로, 여러 루틴이 호출자와 피호출자라는 위아래 관계가 아니라 대등한 관계로 제어를 주고받는다는 뜻이다.
코루틴은 스레드처럼 여러 작업을 번갈아 진행하는 동시성(concurrency)을 표현하지만, 운영체제가 강제로 실행을 빼앗는 선점형이 아니라 코드가 스스로 양보 지점을 정하는 협력적(cooperative) 방식이다. 전환에 커널이 끼어들지 않으므로 비용이 작고, 한 프로세스 안에 수만~수백만 개를 만들 수 있다. 제너레이터, async/await, 가상 스레드처럼 오늘날 흔히 쓰는 지연 계산·비동기 기능이 모두 코루틴을 바탕으로 한다.
현재 "코루틴"은 한 가지 기능이라기보다 여러 구현을 묶어 부르는 이름에 가깝다. Lua의 coroutine, 파이썬의 제너레이터와 async 함수, C++20의 co_await, Kotlin의 suspend 함수, Go의 고루틴이 모두 코루틴이나 그 변형으로 불리지만, 중첩 호출 안에서 멈출 수 있는지, 값을 주고받을 수 있는지, 여러 코어에서 병렬로 도는지가 제각각이다. 코루틴의 성질을 따질 때는 어떤 구현을 전제로 하는지 먼저 밝혀야 한다.
코루틴에 대한 단 하나의 엄밀한 정의는 없다. 1980년 크리스토퍼 말린(Christopher D. Marlin)은 널리 받아들여지는 두 가지 성질을 이렇게 정리했다.
- 코루틴의 지역 데이터 값은 연속된 호출 사이에서 유지된다.
- 제어가 코루틴을 떠나면 실행이 멈추고, 나중에 제어가 다시 들어오면 멈췄던 곳에서 이어서 실행한다.
이 밖에 구현마다 달라지는 축이 세 가지 있다. 아나 루시아 지 모우라(Ana Lúcia de Moura)와 호베르투 이에루잘림스키(Roberto Ierusalimschy, Lua 설계자)는 2009년 논문 "Revisiting Coroutines"에서 이 세 축으로 코루틴을 분류하고, 일급 객체이면서 스택풀인 코루틴을 완전 코루틴(full coroutine)이라 불렀다. 완전 코루틴은 일회성(one-shot) 연속(continuation)이나 한정 연속(delimited continuation)과 표현력이 같다.[1]
| 축 | 선택지 | 뜻 |
|---|---|---|
| 제어 이전 방식 | 대칭 / 비대칭 | 양보할 대상을 직접 지정하는가, 항상 자신을 부른 쪽으로 돌아가는가 |
| 일급 여부 | 일급 객체 / 제한된 구문 | 변수에 담고 함수에 넘기고 반환할 수 있는가 |
| 중단 가능 위치 | 스택풀 / 스택리스 | 코루틴 안에서 부른 일반 함수 깊숙한 곳에서도 멈출 수 있는가 |
대칭 코루틴(symmetric coroutine)에는 "다른 코루틴으로 제어를 넘긴다"는 연산 하나만 있고, 넘길 때마다 목적지를 적어야 한다. 코루틴끼리 완전히 대등하다. 비대칭 코루틴(asymmetric coroutine)에는 resume과 yield 두 연산이 있다. resume으로 코루틴을 깨우고, 코루틴이 yield하면 제어는 언제나 자신을 resume한 쪽으로 돌아간다. 호출·반환 구조와 닮아 쓰기 쉽고 추론하기 쉬워서 Lua, 파이썬, Kotlin 등 대부분의 현대 구현이 비대칭 방식을 택한다. 모우라와 이에루잘림스키는 둘 중 하나로 다른 하나를 구현할 수 있어 표현력은 같다고 보였다.[1]
스택풀 코루틴(stackful coroutine)은 코루틴마다 독립된 호출 스택(call stack)을 가진다. 전환할 때 스택 포인터와 레지스터를 실제로 바꿔 끼우므로, 코루틴 안에서 호출한 일반 함수가 몇 단계 깊이 내려가 있어도 그 자리에서 멈출 수 있다. 대신 코루틴마다 스택 메모리를 미리 잡아야 하고 전환 때 레지스터 저장·복원 비용이 든다. 그래도 커널에 들어가지 않으므로 운영체제 스레드의 문맥 교환보다는 훨씬 싸다.
스택리스 코루틴(stackless coroutine)은 별도 스택이 없다. 컴파일러가 코루틴 함수를 상태 기계(state machine)로 바꾸고, 멈췄을 때 살아 있어야 하는 지역 변수를 힙(또는 호출자 프레임)에 잡은 코루틴 프레임에 넣는다. 전환은 함수 호출 한 번 수준이라 가볍고 메모리도 적게 쓰며, 보통 함수처럼 인라이닝 같은 최적화를 받을 여지도 크다. 하지만 코루틴 함수 본문에서만 멈출 수 있다. 깊은 호출 사슬 안에서 멈추려면 중간 함수가 모두 코루틴이어야 하고, 안쪽의 중단을 바깥으로 전달하는 문법(await, 파이썬의 yield from 등)이 필요하다. 이 때문에 비동기 함수는 비동기 함수에서만 부를 수 있다는 제약이 생기는데, 이를 흔히 "함수 색깔(function color)" 문제라고 부른다.
| 구분 | 스택풀 | 스택리스 |
|---|---|---|
| 스택 | 코루틴마다 독립 스택 | 없음(힙에 코루틴 프레임) |
| 중단 위치 | 중첩 호출 어디서든 | 코루틴 함수 본문에서만 |
| 구현 | 런타임이 스택 전환 | 컴파일러가 상태 기계로 변환 |
| 메모리 | 스택 크기만큼(늘어나는 스택도 있음) | 실제 필요한 변수만큼 |
| 기존 코드와 섞기 | 블로킹 스타일 코드를 그대로 쓰기 쉬움 | async가 호출 사슬 전체로 번짐 |
| 예 | Lua coroutine, Go 고루틴, Java 가상 스레드, Windows 파이버, Ruby·PHP Fiber, Boost.Context 기반 라이브러리 | C++20 코루틴, C# yield·async, 파이썬 제너레이터·async def, 자바스크립트 제너레이터·async, Kotlin suspend 함수, Rust async |
서브루틴은 코루틴의 특수한 경우로 볼 수 있다. yield를 한 번도 하지 않는 코루틴이 곧 서브루틴이다.
| 구분 | 서브루틴 | 코루틴 |
|---|---|---|
| 진입 | 항상 맨 앞에서 시작 | 처음엔 맨 앞, 이후엔 마지막으로 멈춘 곳 |
| 끝 | 한 번 반환하면 끝 | 여러 번 yield하고 마지막에 끝남 |
| 상태 | 호출이 끝나면 지역 변수 소멸 | 멈춘 동안 지역 변수·실행 위치 유지 |
| 관계 | 호출자와 피호출자(위아래) | 서로 제어를 주고받음(대칭 코루틴이면 완전히 대등) |
| 인스턴스 | 호출마다 스택 프레임 하나 | 같은 코루틴을 여러 인스턴스로 동시에 살려 둘 수 있음 |
생산자와 소비자 예가 대표적이다. 생산자는 큐를 채우고 소비자에게 양보하며, 소비자는 큐를 비우고 생산자에게 양보한다. 흔히 스레드 입문 예제로 쓰이지만, 코루틴으로 짜면 스레드 두 개도, 뮤텍스도 필요 없다. yield는 한 루틴에서 다른 루틴으로 곧바로 점프하는 것으로 구현할 수 있다. 서로를 부르는 상호 재귀로도 비슷한 흐름을 만들 수 있지만, 상호 재귀는 호출마다 새 스택 프레임이 쌓이고(꼬리 호출 최적화가 없으면) 상태를 인자나 공유 변수로 넘겨야 한다는 점에서 코루틴보다 불편하다.
제너레이터(generator)는 세미코루틴(semicoroutine)이라고도 하며, 값을 하나씩 만들어 내는 데 특화된 제한된 코루틴이다. 여러 번 멈추고 재개할 수 있다는 점은 코루틴과 같지만, yield하면 제어가 늘 자신을 부른 쪽으로 돌아가며 목적지를 고를 수 없다. 반복자(iterator)를 쉽게 쓰려고 만든 기능이라 yield는 "값을 부모에게 돌려준다"는 의미를 가진다. 그래도 맨 위에 제어를 나눠 주는 디스패처(트램펄린)를 두면 제너레이터만으로 일반 코루틴을 흉내 낼 수 있어, 네이티브 코루틴이 없던 시절의 파이썬 라이브러리가 이 방식을 썼다.
파이썬은 2.2에서 제너레이터를 들여왔고[2], 2.5에서 send()·throw()·close()로 값을 받아들일 수 있게 해 코루틴에 가까워졌으며[3], 3.3에서 하위 제너레이터에 위임하는 yield from을 추가했다.[4] 자바스크립트는 ECMAScript 2015(ES6)의 function*와 yield, C#은 2.0(2005)의 yield return, PHP는 5.5의 yield로 제너레이터를 지원한다.[5][6] C++는 C++23에서 std::generator를 표준 라이브러리에 넣었다.[7]
그린 스레드(green thread)는 커널이 아니라 언어 런타임이 스케줄링하는 스레드다. 기술적으로는 스택풀 코루틴과 거의 같지만 스레드처럼 보이도록 감싼 것이다. 명시적인 중단 키워드 없이 평범한 블로킹 API를 쓰고, 런타임이 I/O 대기 같은 지점에서 알아서 멈추고 다른 작업으로 바꾼다. 그래서 이를 코루틴으로 볼지는 관점에 따라 갈린다.
- Go의 고루틴: go 키워드로 만들며 런타임이 여러 OS 스레드 위에 다중화(M:N)한다. 고루틴 스택은 필요에 따라 늘어나며, Go 1.4부터 새 고루틴의 기본 시작 스택이 8192바이트에서 2048바이트로 줄었다.[10] 고루틴끼리는 보통 채널로 통신한다.
- Java 가상 스레드: Java 21에서 JEP 444로 정식 기능이 되었다(JDK 19·20에서 미리 보기). 가상 스레드는 java.lang.Thread의 인스턴스이지만 실행 내내 OS 스레드를 붙잡지 않고, 블로킹 연산을 만나면 자신을 태우고 있던 캐리어(carrier) 플랫폼 스레드에서 내려온다.[11] 처음에는 synchronized 블록 안에서 블로킹하면 캐리어에 고정(pinning)되는 제약이 있었으나 Java 24의 JEP 491로 대부분 해소되었다.[12]
async/await는 스택리스 코루틴에 프로미스·퓨처(Task, Promise, Future 등)와 작업 스케줄러를 결합한 비동기 프로그래밍 문법이다. 코루틴 그 자체라기보다 "코루틴을 비동기 작업을 기다리는 데 쓰는 패턴"이다. async 함수는 await를 만나면 기다리는 작업이 끝날 때까지 자신만 멈추고 스레드는 다른 일을 하도록 풀어 준다. 컴파일러는 이 함수를 상태 기계와 콜백(연속)으로 바꾼다.
| 연도 | 언어 | 내용 |
|---|---|---|
| 2007 | F# | 계산식(computation expression) 기반의 비동기 워크플로(async { }) 도입, 뒤에 C#에 영향[13] |
| 2012 | C# 5.0 | async·await 키워드 도입(Visual Studio 2012와 함께 출시)[5] |
| 2015 | 파이썬 3.5 | async def·await 문법(PEP 492). 3.4의 asyncio(PEP 3156) 위에서 동작하며, 3.7부터 async·await가 예약어[14] |
| 2015 | TypeScript 1.7 | async/await 지원(ES6 대상)[15] |
| 2017 | 자바스크립트(ES2017) | async 함수 표준화[16] |
| 2018 | Kotlin 1.3 | 코루틴(suspend 함수) 정식 안정화. 1.1(2017)에서는 실험 기능[17] |
| 2019 | Rust 1.39 | async 함수와 .await 안정화(2018 에디션 문법)[18] |
| 2020 | C++20 | co_await, co_yield, co_return 키워드[7] |
| 2021 | Swift 5.5 | async/await, 구조적 동시성, actor[19] |
흔한 오해가 "코루틴은 비동기로, 병렬로 돈다"는 생각이다. 코루틴은 멈춤과 재개라는 제어 흐름 도구일 뿐이다. 누가 언제 어떤 코루틴을 재개할지 정하는 스케줄러가 없으면 코루틴만으로 비동기 프로그래밍을 하기는 어렵고, 스케줄러가 자바스크립트처럼 단일 스레드 루프라면 병렬 실행이 아니다.
비동기 코루틴은 대개 이벤트 루프(event loop) 위에서 돈다. 이벤트 루프는 준비된 코루틴을 하나씩 재개하고, 코루틴이 I/O를 기다리며 양보하면 운영체제의 I/O 다중화(epoll, kqueue, IOCP 등)로 완료를 감시하다가 결과가 오면 해당 코루틴을 다시 실행 대기열에 올린다. 파이썬 asyncio의 이벤트 루프, 브라우저와 Node.js의 이벤트 루프(Node.js는 libuv 사용)가 이런 구조다. 단일 스레드 이벤트 루프에서는 코루틴 하나가 블로킹 호출을 하거나 CPU를 오래 쓰면 루프 전체가 멈춘다는 점을 조심해야 한다.
스레드와 코루틴은 둘 다 동시성을 표현하지만 동작 방식이 다르다. 스레드는 운영체제가 제공하는 선점형 동시성과 병렬 실행에 기대고, 코루틴은 코드가 명시적으로 양보하는 협력형 동작에 기댄다.
| 구분 | 코루틴 | 스레드 |
|---|---|---|
| 스케줄링 | 협력형: 코루틴이 스스로 양보 (비선점 스케줄링과 같은 성격) | 선점형: 운영체제가 언제든 빼앗음 (선점 스케줄링) |
| 관리 주체 | 언어 런타임·라이브러리(사용자 공간) | 운영체제 커널 |
| 전환 비용 | 작음(함수 호출이나 레지스터 몇 개 교환 수준, 시스템 호출 없음) | 큼(커널 진입, 스케줄러, 캐시·TLB 영향) |
| 병렬성 | 한 스레드 안에서는 동시성만 제공 | 여러 코어에서 실제로 병렬 실행 |
| 개수 | 한 스레드에서 수천~수백만 개 | 커널 자원과 스택 메모리에 묶임 |
| 동기화 | 양보 지점 사이는 끼어들 틈이 없어 단일 스레드라면 잠금이 필요 없음 | 언제든 끼어들 수 있어 뮤텍스·세마포어 필요 |
| 알맞은 일 | I/O 대기가 많은 일(네트워크 서버, 크롤러) | CPU를 많이 쓰는 일, 병렬 계산 |
둘은 서로 대신하는 관계라기보다 함께 쓰인다. 대부분의 코루틴 프레임워크(Kotlin, 파이썬 등)는 코루틴을 스레드 위에서 돌린다. 코어가 4개인 기계라면 스레드 4개가 병렬로 돌고, 각 스레드 안에서 여러 코루틴이 동시성을 처리하는 식이다. Kotlin의 디스패처(Dispatchers.Default, Dispatchers.IO), Go 런타임의 M:N 스케줄러, Java 가상 스레드의 캐리어 스레드 풀이 모두 이렇게 코루틴과 스레드를 겹쳐 쓴다. 다만 이렇게 여러 스레드에 코루틴을 흩어 돌리면 코루틴끼리도 데이터 경쟁이 생길 수 있어 다시 동기화가 필요하다.
반대로 선점형 스레드로 코루틴을 구현할 수도 있지만, 그러면 전환 비용이 싸다는 장점과 경성 실시간(hard real-time) 환경에 쓸 수 있다는 장점은 잃는다.
- 한 루틴 안에 든 상태 기계: 현재 진입·탈출 위치가 곧 상태가 되어 goto나 큰 switch 문보다 읽기 쉽다.
- 제너레이터와 스트림: 무한 수열, 파일·네트워크 스트림, 자료 구조의 지연 순회.
- 비동기 I/O: 수많은 연결을 적은 스레드로 처리하는 서버, 크롤러, 채팅.
- 게임 스크립트: 여러 프레임에 걸친 동작(대기, 연출)을 순서대로 적는다. 유니티의 IEnumerator 기반 코루틴이 이 용도로 널리 쓰인다.
- 협력형 멀티태스킹과 행위자 모델 흉내: 각 행위자가 중앙 스케줄러에 스스로 제어를 양보한다.
- CSP 스타일 파이프라인: 채널 입출력에서 멈추는 코루틴을 이어 붙인다.
- 역통신(reverse communication): 수치 해석 루틴이 함수값 계산을 호출자에게 맡길 때.
Lua는 5.0(2003)부터 표준 라이브러리 coroutine으로 일급·스택풀·비대칭 코루틴을 제공한다.[20] 공식 매뉴얼은 이를 "협력적 멀티스레딩"이라 부르며, 코루틴 값의 타입 이름이 thread다. coroutine.create, resume, yield, wrap, status, close 함수로 다룬다.[21]
local co = coroutine.create(function(a, b)
local c = coroutine.yield(a + b) -- 3을 내보내고 멈춤
print("재개 후 받은 값:", c)
return "끝"
end)
print(coroutine.resume(co, 1, 2)) --> true 3
print(coroutine.resume(co, "x")) --> 재개 후 받은 값: x / true 끝
print(coroutine.status(co)) --> dead
제너레이터 기반 코루틴은 yield로 값을 내보내고 send()로 값을 받는다.
def averager():
total = count = 0
avg = None
while True:
x = yield avg # 평균을 내보내고 멈췄다가, send()로 받은 값으로 재개
total += x
count += 1
avg = total / count
co = averager()
next(co) # 첫 yield까지 실행
print(co.send(10)) # 10.0
print(co.send(20)) # 15.0
async def로 만든 네이티브 코루틴은 asyncio 이벤트 루프가 돌린다. 3.11부터는 asyncio.TaskGroup으로 구조적 동시성을 쓸 수 있다.
import asyncio
async def fetch(name, delay):
await asyncio.sleep(delay) # 여기서 양보하면 루프가 다른 코루틴을 돌린다
return f"{name} 완료"
async def main():
results = await asyncio.gather(fetch("A", 1), fetch("B", 2))
print(results) # 약 2초 뒤 ['A 완료', 'B 완료']
asyncio.run(main())
그 밖에 스택풀 방식의 greenlet과 이를 쓰는 gevent·eventlet, 인터프리터를 고친 Stackless Python이 있다.
function* idGen() { // 제너레이터(ES2015)
let id = 0;
while (true) yield ++id;
}
const g = idGen();
g.next().value; // 1
g.next().value; // 2
async function load() { // async 함수(ES2017)
const res = await fetch("/api/data"); // 프로미스가 끝날 때까지 이 함수만 멈춘다
return res.json();
}
ES2015 이전에는 콜백으로 비동기를 처리했고(이른바 콜백 지옥), ES2015에서 Promise가 표준이 되었으며, ES2017에서 async/await가 더해졌다.
Kotlin은 언어에 suspend 키워드만 두고, launch·async·runBlocking 같은 빌더는 공식 라이브러리 kotlinx.coroutines가 제공한다. 컴파일러는 suspend 함수에 연속(Continuation) 매개변수를 덧붙여 상태 기계로 바꾼다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val jobs = List(100_000) {
launch {
delay(1000L) // 스레드를 막지 않고 코루틴만 멈춘다
}
}
jobs.joinAll()
println("코루틴 10만 개 완료")
}
C++20 코루틴은 스택리스이며 멈춘 상태는 보통 힙에 할당한 코루틴 프레임에 저장된다. 함수 본문에 co_await, co_yield, co_return 가운데 하나가 있으면 코루틴이 된다. 표준은 언어 장치만 정하고, 반환 타입의 promise_type과 await_ready·await_suspend·await_resume을 갖춘 대기 가능(awaitable) 타입은 라이브러리가 만든다. C++23에서 std::generator가 추가되었다.[7]
#include <generator>
#include <print>
#include <ranges>
std::generator<int> fib() {
int a = 0, b = 1;
while (true) {
co_yield a; // 값을 내보내고 멈춤
int t = a + b; a = b; b = t;
}
}
int main() {
for (int x : fib() | std::views::take(10))
std::print("{} ", x); // 0 1 1 2 3 5 8 13 21 34
}
표준 이전에는 Boost.Context(여러 CPU와 운영체제에서 문맥 전환 지원)와 그 위의 Boost.Coroutine2 같은 스택풀 라이브러리를 썼다.
C# 2.0의 반복자(yield return)가 제너레이터 역할을 했고, C# 5.0에서 async/await가 들어왔다. C# 8.0에서는 비동기 스트림(await foreach)이 추가되었다.[5]
ch := make(chan int)
for i := 0; i < 3; i++ {
go func(n int) { ch <- n * n }(i) // 고루틴 3개
}
for i := 0; i < 3; i++ {
fmt.Println(<-ch) // 순서는 보장되지 않음
}
// Java 21 가상 스레드: 블로킹 코드를 그대로 쓰되 스레드 1만 개를 싸게 만든다
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // 이 동안 캐리어 스레드는 다른 가상 스레드를 돌린다
return i;
}));
}
Java 21 이전에는 언어 차원의 코루틴이 없어 바이트코드를 고쳐 쓰는 라이브러리를 쓰거나 Kotlin 코루틴을 CompletableFuture로 감싸 불렀다.
| 언어 | 형태 | 비고 |
|---|---|---|
| Simula 67, Modula-2 | 언어 내장 | 초기 고급 언어 지원. Modula-2는 SYSTEM 모듈의 NEWPROCESS·TRANSFER |
| Lua | 스택풀 비대칭 | 5.0(2003)부터 |
| 파이썬 | 제너레이터, async def | 2.2 제너레이터, 2.5 send, 3.3 yield from, 3.5 async/await |
| C# | 반복자, async | 2.0 yield, 5.0 async/await |
| 자바스크립트 | 제너레이터, async 함수 | ES2015, ES2017 |
| PHP | 제너레이터, Fiber | 5.5 제너레이터, 8.1 Fiber |
| Ruby | Fiber | 스택풀 |
| Kotlin | suspend 함수 | 1.1 실험, 1.3 안정 |
| Go | 고루틴 | 그린 스레드. 엄밀한 코루틴과는 다름 |
| Rust | async/.await | 1.39. 실행기(executor)는 tokio 같은 외부 라이브러리가 제공 |
| C++ | 스택리스 | C++20, C++23 std::generator |
| Java | 가상 스레드 | 21 |
| Swift | async/await | 5.5 |
| Scheme, Racket, Smalltalk | 연속·일급 스택으로 구현 | 언어가 연속을 지원하면 코루틴 구현이 쉽다 |
C 언어에는 코루틴이 없다. 일반 코루틴을 만들려면 두 번째 호출 스택이 필요한데, C는 이를 직접 지원하지 않는다. 흔한 방법은 다음과 같다.
- ucontext: getcontext, makecontext, swapcontext로 스택과 문맥을 바꾼다. POSIX.1-2008에서 이식성 문제로 규격에서 빠졌지만 glibc 등은 여전히 제공한다.[22]
- setjmp/longjmp와 약간의 인라인 어셈블리로 스택 포인터를 바꾼다. 러스 콕스(Russ Cox)의 libtask 같은 라이브러리가 이런 세부를 숨긴다.
- 사이먼 태텀(Simon Tatham)이 소개한 더프의 장치(Duff's device) 기반 매크로와 이를 발전시킨 Protothreads: 스택 없이 switch 문으로 재진입한다. 가볍지만 yield를 넘어 지역 변수가 보존되지 않고 최상위 함수에서만 양보할 수 있다.
- 1958년: 멜빈 콘웨이(Melvin Conway)가 어셈블리 프로그램을 짜며 코루틴이라는 용어를 만든 것으로 알려져 있다. 도널드 커누스가 이렇게 전했다.
- 1963년: 콘웨이가 논문 "Design of a Separable Transition-Diagram Compiler"(Communications of the ACM 6권 7호)에서 코루틴을 처음 글로 설명했다. 한 번에 훑어 컴파일하는 COBOL 컴파일러의 어휘 분석기와 구문 분석기를 서로 양보하는 코루틴으로 짠 설계였다.[23]
- 1960~1970년대: Simula 67, BLISS, BCPL, Modula-2 등이 코루틴을 언어나 표준 라이브러리에 넣었다. 커누스의 "The Art of Computer Programming" 1권도 코루틴을 기본 기법으로 다루었다.
- 1980년: 말린이 코루틴의 두 성질을 정리했다.
- 1990~2000년대: 스택 기반 호출 구조가 주류가 되고 스레드가 널리 쓰이면서 C 계열 주류 언어에서는 코루틴이 한동안 밀려났다. 대신 파이썬 2.2(2001)의 제너레이터, C# 2.0(2005)의 반복자, Lua 5.0(2003)이 명맥을 이었다.
- 2007~2012년: F# 비동기 워크플로와 C# 5.0 async/await가 나오며 비동기 I/O를 위한 코루틴이 다시 주목받았다.
- 2009년: 모우라와 이에루잘림스키가 "Revisiting Coroutines"에서 분류 체계와 완전 코루틴 개념을 정리했다.[1]
- 2015년 이후: 파이썬, 자바스크립트, Kotlin, Rust, C++, Swift가 차례로 async/await나 코루틴을 표준에 넣었고, 2023년 Java 21이 가상 스레드를 정식 도입했다.[11]
- 블로킹 호출: 단일 스레드 이벤트 루프에서 동기 I/O나 time.sleep 같은 블로킹 함수를 부르면 모든 코루틴이 함께 멈춘다. 비동기 버전 API를 쓰거나 별도 스레드 풀로 넘겨야 한다.
- CPU 작업: 코루틴은 병렬성을 주지 않는다. 계산이 많은 일은 여러 스레드나 프로세스로 나눠야 한다.
- 함수 색깔: 스택리스 방식에서는 async가 호출 사슬 전체로 번지고, 동기 코드와 비동기 코드를 섞기 번거롭다.
- 취소와 예외: 멈춘 코루틴을 언제 어떻게 취소할지, 자식 코루틴의 예외를 누가 받을지가 까다롭다. 이를 정리하려고 Kotlin의 coroutineScope, 파이썬의 TaskGroup, Java의 StructuredTaskScope 같은 구조적 동시성 도구가 나왔다.
- 디버깅: 호출 스택이 코루틴 경계에서 끊겨 스택 추적이 읽기 어렵다.
- 메모리: 스택풀 코루틴은 코루틴마다 스택을 잡아야 하고, 스택리스 코루틴도 멈춘 동안 참조하는 객체를 붙잡아 둔다.
- 데이터 경쟁: 여러 스레드 위에 코루틴을 흩어 돌리는 디스패처를 쓰면 코루틴끼리도 경쟁 상태가 생긴다. "코루틴이니 잠금이 필요 없다"는 말은 단일 스레드일 때만 맞다.
- 고정 문제: Java 21 가상 스레드는 synchronized 안에서 블로킹하거나 네이티브 메서드를 호출하는 동안 캐리어 스레드에 고정되었다. synchronized 쪽은 Java 24에서 대부분 해결되었다.[11]
- ↑ 1.0 1.1 1.2 Ana Lúcia de Moura, Roberto Ierusalimschy, "Revisiting Coroutines", ACM TOPLAS 31(2), 2009
- ↑ PEP 255: Simple Generators
- ↑ PEP 342: Coroutines via Enhanced Generators
- ↑ PEP 380: Syntax for Delegating to a Subgenerator
- ↑ 5.0 5.1 5.2 The history of C#, Microsoft Learn
- ↑ PHP Manual: Generators overview
- ↑ 7.0 7.1 7.2 Coroutines (C++20), cppreference
- ↑ Fibers, Win32 apps, Microsoft Learn
- ↑ PHP Manual: Fibers
- ↑ Go 1.4 Release Notes
- ↑ 11.0 11.1 11.2 JEP 444: Virtual Threads
- ↑ JEP 491: Synchronize Virtual Threads without Pinning
- ↑ Don Syme, "The Early History of F#", HOPL IV, 2020
- ↑ PEP 492: Coroutines with async and await syntax
- ↑ TypeScript 1.7 Release Notes
- ↑ TC39 Finished Proposals
- ↑ What's new in Kotlin 1.3
- ↑ Announcing Rust 1.39.0
- ↑ Swift 5.5 Released!
- ↑ Lua: version history
- ↑ Lua 5.4 Reference Manual, 2.6 Coroutines
- ↑ makecontext(3), Linux manual page
- ↑ Melvin E. Conway, "Design of a separable transition-diagram compiler", CACM 6(7), 1963