fork
소스를 한 번만 순회해 버퍼에 담고, 이를 독립적이고 다시 읽을 수 있는 리더들로 분기합니다.
강의
같은 Dart Iterable 객체를 두 번 순회하면 보통 소스도 두 번
돌아갑니다 — sync* 제너레이터는 새 .iterator를
요청할 때마다 처음부터 다시 시작하니까요. 값을 만드는 비용이 비쌀 때
이는 낭비이거나 아예 잘못된 동작입니다. 네트워크 요청, 느린 계산,
한 번만 읽을 수 있는 스트림이 그렇죠. fork가 이 문제를
해결합니다. fork(iterable)을 같은
iterable 객체로 호출할 때마다, 공유되며 지연 방식으로
커지는 하나의 버퍼 위에 독립적인 커서가 만들어집니다. 몇 개의 fork가
어떤 순서로 읽든 하부 소스는 정확히 한 번만 순회됩니다.
공유는 넘겨받은 iterable의 동일성(내부적으로
Expando를 사용)을 키로 하므로, 반드시 같은 객체를
fork해야 합니다 — 겉보기에 똑같아 보이더라도 따로 만든 두 이터러블은
안 됩니다. 각 fork는 자기 속도대로 소비할 수 있습니다. 한 fork가 앞서
나가면 소스에서 새 값을 끌어와 공유 버퍼에 덧붙이고, 뒤처진 fork는
이미 버퍼에 있는 값을 추가 비용 없이 다시 읽을 뿐입니다.
forkAsync는 FxAsyncIterable에 대해 같은 방식으로
동작하며, 여러 fork에서 동시에 발생한 하류 요구가 공유 비동기 소스를
병렬로 끌어당기게 해 줍니다.
데모 1 · 소스 하나, 분기 둘, 카운터로 증명하기
source()는 값을 만들 때마다 calls를 증가시킵니다.
evens와 doubled는 완전히 동일한
shared 객체를 fork합니다 — 소스가 두 번 돌았다면
calls는 5가 아니라 10이 되었을 겁니다:
데모 2 · 속도가 다른 fork들이 버퍼 하나를 공유합니다
분기 a가 앞서 달려 새 값 두 개를 끌어옵니다. 분기
b가 처음 두 값을 요청하면 a가 이미 버퍼에 담아 둔
값을 그대로 다시 읽습니다 — 두 fork 모두 아직 보지 못한 세 번째 값을
b가 필요로 하기 전까지는 source() 호출이
새로 일어나지 않습니다:
직접 해 보기
연습: 지금은 readings가 fork 없이 두 번
순회되어 sensor()가 두 번 실행되고 reads가
6이 됩니다. 각 소비자마다 readings를 fork해서 센서를
한 번만 읽도록 만들어 보세요(reads는 3이 되어야 합니다).