concurrent

비동기 파이프라인의 원소를 최대 n개까지 한 번에 평가하면서도, 결과는 소스 순서 그대로 도착합니다.

FxAsyncIterable<A> concurrentAsync<A>(int length, FxAsyncIterable<A> iterable) FxAsync<T> FxAsync.concurrent(int length) // chain

강의

concurrent(n)은 "여러 비동기 단계를 병렬로 실행하되 결과 순서는 유지하고 싶다"는 요구에 대한 FxDart의 답입니다. 동작의 핵심은 동시성 마커 모델이며, 이는 FxAsyncIterator.next([Concurrent? concurrent])에 내장되어 있습니다. .concurrent(3)을 호출하면, 이 연산자를 거치는 모든 pull이 Concurrent(3) 마커를 상류로 한 단계씩 전달하면서 값을 만들어 내는 쪽에게 "하나씩 말고 한 번에 3개를 평가하라"고 알립니다. 상류 연산자(보통은 지연 평가되는 map)는 이 마커를 보고, Future 하나를 await한 뒤 그다음 것을 시작하는 대신 자기 소스의 next()를 중간에 기다리지 않고 세 번 호출합니다. 그 결과 Future 세 개가 동시에 진행됩니다. 각각이 완료되는 대로 concurrent는 결과를 버퍼에 담아 두고, 호출자에게는 원래 순서대로만 값을 내보냅니다. 그래서 뒤쪽 항목이 앞쪽보다 먼저 끝나더라도 결과는 언제나 입력 순서와 일치합니다.

이것이 toAsync 강의에서 언급했던 역채널입니다. Dart의 Stream은 나중에 상류 소스에게 "한 번에 3개를 달라"고 요청할 방법이 없습니다. Stream은 자기 페이스대로 값을 밀어내기 때문입니다. FxDart의 pull 기반 next() 프로토콜은 매 pull마다 그 요청을 상류로 실어 나르며, 이것이 바로 concurrent(n)을 가능하게 하는 기반입니다.

n은 무엇에 병목이 걸리는지에 따라 조절하세요. REST API라면 동시 요청 5~10개 정도는 견딜 수 있고, n = 1은 순차적으로 await하는 것과 같으며, 이는 concurrent를 아예 쓰지 않았을 때의 동작과 정확히 같습니다. CPU 일은 parallel이지, 더 큰 n이 아닙니다 — concurrent or parallel을 보세요.

지연이 0인 fetch N=100k에서, 손으로 짠 워커 풀이 여전히 약 10% 더 빠릅니다 (AOT). 남은 값은 순서 있는 배치 장치이지 map 층이 아닙니다 — 체인을 쓰는 대가입니다.

데모 1 · 순차 실행과 concurrent(3) 비교, 시간 측정

각각 200ms가 걸리는 항목 6개입니다. 순차 실행은 약 1200ms가 걸리지만, 한 번에 3개씩 요청하면 약 400ms로 줄어듭니다.

데모 2 · 완료 순서가 달라도 결과 순서는 유지됩니다

여기서는 항목 2가 항목 1보다 먼저 끝나지만(100ms 대 300ms), concurrent(3)은 여전히 소스 순서대로 결과를 돌려줍니다. 정반대로 동작하는 concurrentPool과 비교해 보세요.

직접 해 보기

연습: 이 파이프라인은 각각 200ms가 걸리는 항목 6개를 순차적으로 (n = 1) 처리합니다. n을 올려 가며 경과 시간이 줄어드는 것을 확인해 보세요. 3으로, 그다음 6으로 바꿔 보세요.

관련 항목: concurrentPool — 완료 순서로 내보내는 변형 · toAsync — 이 동작이 기반으로 삼는 pull 기반 모델 · async 변형 — *Async 명명 규칙 · map — concurrent와 가장 자주 함께 쓰이는 연산자 · concurrent or parallel — I/O vs CPU