* Пондер однажды пересчитал муравьёв в Гексе. Вышло четыре тысячи двести. Назавтра пересчитал снова: четыре тысячи двести. Он долго радовался, пока не понял, что муравьи бегают по кругу и он считал одних и тех же, а остальные в это время сидели в трубках и ждали своей очереди.
Всё, что башня делала до сих пор, шло одной дорожкой: кадр за кадром, шаг за шагом. Даже ночь с сотней строк была одним муравьём, который бежал от первой строки к последней. Рыдкулл хочет, чтобы башня отбивала пульс, пока идёт смена: раз в секунду строку в журнал, чтобы утром было видно, что башня жила всю ночь. Пульс не может ждать, пока смена кончится, и смена не может ждать пульса.
Файбер
Файбер это квитанция, которую запустили отдельной дорожкой. Слово значит «волокно»: тонкая нить исполнения, гораздо легче потока операционной системы. В Гексе файбер это муравей. Исполнитель Effect бегает между файберами и даёт каждому сделать несколько шагов; как он выбирает, разберём в уроке 70.
Ты уже запускал файберы, не называя их: Effect.runFork в уроке 45 и runMain в уроке 58 запускали главный файбер программы. Внутри квитанции файбер запускают через Effect.forkChild:
export const beat: Effect.Effect<never, never, Journal> = Effect.gen(function* () {
const journal = yield* Journal;
for (let count = 1; ; count++) {
yield* Effect.sleep('1 second');
yield* journal.write(`пульс ${count}`);
}
});
Пульс это бесконечный цикл: успеха у него нет, поэтому первый параметр never. Если запустить его как обычный шаг, yield* beat, смена до следующего шага не дойдёт никогда. Его запускают рядом:
export const withHeartbeat = <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | Journal> =>
Effect.gen(function* () {
const pulse = yield* Effect.forkChild(beat);
return yield* self.pipe(Effect.ensuring(Fiber.interrupt(pulse)));
});
yield* Effect.forkChild(beat) запускает пульс и сразу отдаёт его файбер, Fiber<never, never>. Смена идёт дальше, не дожидаясь пульса. Когда смена кончилась, чем бы ни кончилась, ensuring из урока 57 прерывает пульс.
const night = Effect.as(Effect.sleep('2500 millis'), 'ночь прошла');
const journal = await Effect.runPromise(
Effect.gen(function* () {
const result = yield* withHeartbeat(night);
yield* Effect.sleep('2 seconds');
return [result, ...(yield* Journal.use((journal) => journal.entries))];
}).pipe(Effect.provide(Journal.layer)),
);
[ 'ночь прошла', 'пульс 1', 'пульс 2' ]
Ночь шла две с половиной секунды, пульс ударил дважды. После ночи программа ждала ещё две секунды, а пульсов больше нет: его прервали вместе с концом смены.
Перевод с Ук
Первая попытка, без звёздочки:
const pulse = Effect.forkChild(beat);
return yield* self.pipe(Effect.ensuring(Fiber.interrupt(pulse)));
ex61c.ts(9,61): error TS2345: Argument of type 'Effect<Fiber<never, never>, never, Journal>' is not assignable to parameter of type 'Fiber<unknown, unknown>'.
Type 'Effect<Fiber<never, never>, never, Journal>' is missing the following properties from type 'Fiber<unknown, unknown>': id, currentOpCount, getRef, context, and 7 more.
Effect.forkChild(beat) это квитанция, которая запустит пульс и вернёт файбер. Пока её не исполнили через yield*, никакого файбера нет, есть только запись «запусти». Fiber.interrupt хочет сам файбер, а получил запись. Вторая строка перечисляет, чего не хватает: у квитанции нет id, нет context и ещё семи полей файбера. Лечится звёздочкой: const pulse = yield* Effect.forkChild(beat).
Что умеет файбер
У файбера четыре главные операции, и все они квитанции.
Fiber.join(fiber)ждёт конца и отдаёт результат, а провал файбера становится провалом того, кто ждёт:Effect<A, E>.Fiber.await(fiber)ждёт конца и отдаётExitзначением, никогда не падая:Effect<Exit<A, E>>.Fiber.interrupt(fiber)прерывает и ждёт, пока прерванный закроет свои финализаторы.fiber.pollUnsafe()спрашивает без ожидания:undefined, если ещё работает, иначеExit. Нужен в тестах, в программе обычно ждут.
Исход файбера одним словом башня теперь умеет говорить сама, через exitWord склада из урока 55:
export const outcomeOf = <A, E>(fiber: Fiber.Fiber<A, E>): Effect.Effect<string> =>
Effect.map(Fiber.await(fiber), exitWord);
const fiber = Effect.runFork(Effect.as(Effect.sleep('100 millis'), 42));
console.log(await Effect.runPromise(Fiber.join(fiber)));
const sleeper = Effect.runFork(Effect.never);
await Effect.runPromise(Fiber.interrupt(sleeper));
console.log(await Effect.runPromise(outcomeOf(sleeper)));
42
interrupt
Дерево файберов
Файбер, запущенный через forkChild, это ребёнок того, кто его запустил. У ребёнка могут быть свои дети. Выходит дерево, и прерывание идёт по нему вниз: прервали смену, остановились пульс, кадры и ответы, которые запустили кадры. Вверх и вбок прерывание не идёт: прервали ответ на кадр 5, смена и соседний кадр работают дальше.
Почему ребёнок умирает с родителем, и бывает ли иначе, разберём в следующем уроке. Пока хватит правила: forkChild запускает работу, которая не переживёт того, кто её начал.
Под капотом
Наш Гекс из урока 33 исполнял одну квитанцию: брал инструкцию, исполнял, клал продолжение на стек. Файбер Effect устроен так же, только стеков много, по одному на файбер, и исполнитель переключается между ними. Переключение дешёвое: это не поток операционной системы, а объект в памяти со своим стеком продолжений. Поэтому файберов можно завести тысячи. Делать работу одновременно в смысле «на двух ядрах сразу» они не умеют: JavaScript исполняет один шаг за раз. Они умеют не ждать: пока один спит или ждёт провода, другой работает.
Где тут дыра
const shift = Effect.gen(function* () {
Effect.runFork(beat);
yield* Effect.sleep('1500 millis');
log.push('смена кончилась');
});
await Effect.runPromise(shift);
await new Promise((resolve) => setTimeout(resolve, 2100));
console.log(log);
[ 'пульс', 'смена кончилась', 'пульс', 'пульс' ]
Смена кончилась, а пульс бьётся дальше. Effect.runFork внутри квитанции это ещё один край программы посреди программы, как runPromise Ринцвальда из урока 45. Файбер ничей: его не прервёт ни конец смены, ни сигнал из урока 58, и тест, который его запустил, оставит пульс биться в следующих тестах. Ещё он не видит контекста: журнал, провод и тестовые часы туда не попадут.
Лечится тем, что внутри квитанции файберы запускают через forkChild, и они становятся частью дерева.
Башня, этап 26
У башни теперь есть пульс. Он бьётся в фоне раз в секунду, пока идёт работа, и гаснет вместе с ней, чем бы она ни кончилась. А исход любого файбера башня называет одним словом, как склад.
Практика
Дальше
Рыдкулл читает утренний журнал и хмурится.
Чьи это дети и кто из них может пережить родителя, разберём в следующем уроке.