Раздел 46 · Effect: Тихая Академия

Общее состояние: Ref

middle~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Пока файбер был один, общее состояние было безопасным само собой: журнал из урока 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

Башня получает общую память для многих файберов. Список виденных кадров, где проверка и запись это один шаг, и счёт по словам, в который сто файберов пишут без потерь.

Практика

Дальше

Рыдкулл заходит в подвал среди ночи.

Ворота и ожидание сигнала ждут в следующем уроке.