Пока файбер был один, общее состояние было безопасным само собой: журнал из урока 48 жил в обычном массиве, и никто не мешал ему писать. С сотней файберов у одного и того же значения сотня хозяев, и каждый думает, что он один.
Сто муравьёв и счёт
let plain = 0;
await Effect.runPromise(
Effect.forEach(hundred, () => Effect.gen(function* () {
const seen = plain;
yield* Effect.yieldNow;
plain = seen + 1;
}), { concurrency: 'unbounded' }),
);
console.log('let:', plain);
let: 1
Не девяносто семь, а один. Effect.yieldNow между «посмотреть» и «написать» отдаёт очередь соседу; как и зачем, расскажет урок 70. Сто муравьёв подошли к счёту по очереди, каждый увидел ноль, и каждый записал единицу. Это потерянное обновление. В жизни вместо yieldNow будет вопрос к книге отправленных или ответ провода, и окно между чтением и записью будет шире.
Ref
Ref это ячейка, с которой работают квитанциями: Ref.make(0), Ref.get(ref), Ref.set(ref, значение). Сама по себе ячейка от гонки не спасает:
Ref.get и Ref.set: 1 Ref.update: 100
Та же сотня с Ref.get, yieldNow и Ref.set теряет те же девяносто девять. Спасает одна операция вместо двух: Ref.update(ref, (n) => n + 1). Ячейка исполняет функцию и записывает результат, и между ними никто не встанет. Сто обновлений, сто в счёте.
У update есть старший брат, Ref.modify(ref, f). Функция возвращает пару: что отдать тому, кто спросил, и новое значение ячейки. Так башня спрашивает «видел ли я этот кадр» и тут же запоминает его:
export class Seen extends Context.Service<
Seen,
{ readonly firstTime: (key: FrameKey) => Effect.Effect<boolean> }
>()('bashnya/Seen') {
static readonly layer = Layer.effect(
Seen,
Effect.map(Ref.make(HashSet.empty<FrameKey>()), (ref) =>
Seen.of({
firstTime: (key) =>
Ref.modify(ref, (seen) =>
HashSet.has(seen, key) ? [false, seen] : [true, HashSet.add(seen, key)],
),
}),
),
);
}
Проверка и запись одним шагом: из ста одновременных firstTime для одного кадра ответ true получит ровно один. Неизменяемый HashSet из урока 44 здесь не случайность: ячейка хранит значение, а не объект, который кто-то может поменять за её спиной.
SynchronizedRef
Функция в update и modify обычная, без эффектов. Иногда новое значение нельзя посчитать, не спросив кого-то: книгу, соседа, склад. Тогда нужен SynchronizedRef: его updateEffect и modifyEffect принимают функцию, которая возвращает квитанцию, и не пускают к ячейке никого, пока квитанция не кончилась:
const sent = yield* SynchronizedRef.make<readonly number[]>([]);
const ask = (id: number) => Effect.as(Effect.sleep('10 millis'), id % 2 === 0);
yield* Effect.forEach([1, 2, 3, 4], (id) =>
SynchronizedRef.updateEffect(sent, (ids) => Effect.map(ask(id), (ok) => (ok ? [...ids, id] : ids))),
{ concurrency: 'unbounded' });
SynchronizedRef: [ 2, 4 ]
Четыре обновления шли одновременно, каждое ждало ответа, и ни одно не потерялось. Цена в том, что пока одно ждёт ответа, остальные ждут его: обновления идут гуськом. Поэтому SynchronizedRef берут, только когда без эффекта в обновлении правда не обойтись.
SubscriptionRef
Третий вид ячейки умеет говорить о своих изменениях. SubscriptionRef это Ref, у которого есть changes: поток новых значений, на который можно подписаться. Статус башни, «на связи», «занята», «молчит», держат так: кто хочет знать, подписывается. Потоки это EF10, поэтому сейчас только запомни, что такая ячейка есть.
Под капотом
JavaScript исполняет один шаг за раз, и исполнитель переключает файберы только между шагами квитанции. Ref.update это один шаг: прочитать, посчитать, записать. Никто не вклинится в середину. Ref.get, потом Ref.set это два шага, и между ними переключение возможно. Отсюда правило: всё, что должно быть неделимым, пусть будет одним update или modify. У SynchronizedRef внутри замок, семафор на одно разрешение, о котором будет урок 67.
Где тут дыра
Дыра урока разобрана в начале: Ref.get, затем ожидание, затем Ref.set. Её легко не заметить, потому что на одном файбере она не видна, а на десяти даёт о себе знать не каждую ночь. Ищи её по признаку: между чтением общего значения и записью стоит yield*. Если стоит, это или один modify, или SynchronizedRef.
Башня, этап 30
Башня получает общую память для многих файберов. Список виденных кадров, где проверка и запись это один шаг, и счёт по словам, в который сто файберов пишут без потерь.
Практика
Дальше
Рыдкулл заходит в подвал среди ночи.
Ворота и ожидание сигнала ждут в следующем уроке.