Перейти к содержимому
Теория · Асинхронный код · урок 6 из 7

Акторы

В кафе одна тетрадь заказов. Представьте, что к ней одновременно подошли два кассира. Оба прочитали последний номер, 12, и оба записали свой заказ под номером 13. Один заказ потерялся.

Чтобы так не случалось, у тетради работает один кассир, и он обслуживает одного покупателя за раз. Остальные покупатели ждут своей очереди, и тетрадь всегда в порядке.

Гонка данных

В программе то же самое случается, когда две работы идут одновременно и обе меняют одну переменную. Гонка данных — одновременное обращение двух частей программы к одной переменной, когда хотя бы одна из них её меняет. Результат зависит от того, кто успел первым, и изменения могут теряться.

Найти такую ошибку трудно: программа может сто раз работать верно и сломаться на сто первый. Поэтому Swift 6 ищет гонки данных ещё при компиляции. По той же причине функция не может менять переменную из главной части программы.

Компилятор следит и за классами. Экземпляр класса со счётчиком нельзя передать в две работы async let. Такая программа не скомпилируется: класс не защищает свои свойства от одновременных изменений.

Актор

Актор — ссылочный тип, который обслуживает только одну задачу за раз, поэтому две задачи никогда не меняют его свойства одновременно. Он похож на класс, а объявляют его словом actor вместо class:

SWIFT
actor OrderBook {
    var count = 0

    func addOrder() {
        count += 1
    }
}

let book = OrderBook()
await book.addOrder()
await book.addOrder()
print(await book.count) // 2

Разберём программу по строкам:

  1. actor OrderBook объявляет актор со свойством count и методом addOrder. Внутри актора метод меняет count напрямую, без await.
  2. let book = OrderBook() создаёт экземпляр. Актор — ссылочный тип, поэтому let закрепляет только ссылку, а count менять можно.
  3. await book.addOrder() вызывает метод снаружи актора, поэтому нужен await: если актор занят другой задачей, вызов подождёт своей очереди.
  4. print(await book.count) читает свойство count снаружи, тоже через await, и печатает 2. Чтение ждёт очереди, потому что count — свойство var и его меняют вызовы addOrder.

Две работы и один актор

Теперь пусть две работы одновременно принимают заказы, по тысяче заказов каждая:

SWIFT
func takeOrders(_ book: OrderBook, times: Int) async -> Int {
    for _ in 1...times {
        await book.addOrder()
    }
    return times
}

async let first = takeOrders(book, times: 1000)
async let second = takeOrders(book, times: 1000)
let taken = await first + second
print(taken, await book.count) // 2000 2002
  1. takeOrders вызывает addOrder столько раз, сколько попросили, и каждый раз через await.
  2. Две задачи async let вызывают takeOrders одновременно и пишут в одну и ту же тетрадь.
  3. Актор пропускает вызовы addOrder по одному, поэтому ни один заказ не теряется: к двум прежним добавилось ровно 2000.

Запускайте программу сколько угодно раз: она всегда напечатает 2000 2002.

Здесь картинка с кассиром неточна. Кассир доводит заказ до конца и только потом зовёт следующего покупателя. А если метод актора сам ждёт на await, актор на это время пускает к себе следующую задачу.

Этот вопрос часто задают на собеседовании: что такое актор и чем вызов его метода отличается от вызова метода класса? Ответ:

  • актор — ссылочный тип, который обслуживает одну задачу за раз, поэтому две задачи не меняют его свойства одновременно;
  • метод класса вызывают напрямую, а метод актора снаружи — через await: вызову, возможно, придётся подождать очереди.

Частая ошибка

Метод актора вызвали снаружи без await:

SWIFT
book.addOrder() // ✗
TEXT
error: actor-isolated instance method 'addOrder()' cannot be called from outside of the actor

Программа не компилируется. Сообщение звучит так, будто метод актора снаружи вызывать нельзя совсем, но дело в пропущенном await: строка await book.addOrder() компилируется.

Главное

  • Гонка данных — одновременное обращение двух частей программы к одной переменной, когда хотя бы одна её меняет; изменения при этом могут теряться.
  • Актор — ссылочный тип, который обслуживает одну задачу за раз; его объявляют словом actor.
  • Снаружи метод актора вызывают и свойство var читают через await, а внутри актора — напрямую.
  • Swift 6 ищет гонки данных при компиляции: вызов актора снаружи без await не скомпилируется.