Акторы
В кафе одна тетрадь заказов. Представьте, что к ней одновременно подошли два кассира. Оба прочитали последний номер, 12, и оба записали свой заказ под номером 13. Один заказ потерялся.
Чтобы так не случалось, у тетради работает один кассир, и он обслуживает одного покупателя за раз. Остальные покупатели ждут своей очереди, и тетрадь всегда в порядке.
Гонка данных
В программе то же самое случается, когда две работы идут одновременно и обе меняют одну переменную. Гонка данных — одновременное обращение двух частей программы к одной переменной, когда хотя бы одна из них её меняет. Результат зависит от того, кто успел первым, и изменения могут теряться.
Найти такую ошибку трудно: программа может сто раз работать верно и сломаться на сто первый. Поэтому Swift 6 ищет гонки данных ещё при компиляции. По той же причине функция не может менять переменную из главной части программы.
Компилятор следит и за классами. Экземпляр класса со счётчиком нельзя передать в две работы async let. Такая программа не скомпилируется: класс не защищает свои свойства от одновременных изменений.
Актор
Актор — ссылочный тип, который обслуживает только одну задачу за раз, поэтому две задачи никогда не меняют его свойства одновременно. Он похож на класс, а объявляют его словом actor вместо class:
actor OrderBook {
var count = 0
func addOrder() {
count += 1
}
}
let book = OrderBook()
await book.addOrder()
await book.addOrder()
print(await book.count) // 2Разберём программу по строкам:
actor OrderBookобъявляет актор со свойствомcountи методомaddOrder. Внутри актора метод меняетcountнапрямую, безawait.let book = OrderBook()создаёт экземпляр. Актор — ссылочный тип, поэтомуletзакрепляет только ссылку, аcountменять можно.await book.addOrder()вызывает метод снаружи актора, поэтому нуженawait: если актор занят другой задачей, вызов подождёт своей очереди.print(await book.count)читает свойствоcountснаружи, тоже черезawait, и печатает2. Чтение ждёт очереди, потому чтоcount— свойствоvarи его меняют вызовыaddOrder.
Две работы и один актор
Теперь пусть две работы одновременно принимают заказы, по тысяче заказов каждая:
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 2002takeOrdersвызываетaddOrderстолько раз, сколько попросили, и каждый раз черезawait.- Две задачи
async letвызываютtakeOrdersодновременно и пишут в одну и ту же тетрадь. - Актор пропускает вызовы
addOrderпо одному, поэтому ни один заказ не теряется: к двум прежним добавилось ровно 2000.
Запускайте программу сколько угодно раз: она всегда напечатает 2000 2002.
Здесь картинка с кассиром неточна. Кассир доводит заказ до конца и только потом зовёт следующего покупателя. А если метод актора сам ждёт на await, актор на это время пускает к себе следующую задачу.
Этот вопрос часто задают на собеседовании: что такое актор и чем вызов его метода отличается от вызова метода класса? Ответ:
- актор — ссылочный тип, который обслуживает одну задачу за раз, поэтому две задачи не меняют его свойства одновременно;
- метод класса вызывают напрямую, а метод актора снаружи — через
await: вызову, возможно, придётся подождать очереди.
Частая ошибка
Метод актора вызвали снаружи без await:
book.addOrder() // ✗error: actor-isolated instance method 'addOrder()' cannot be called from outside of the actorПрограмма не компилируется. Сообщение звучит так, будто метод актора снаружи вызывать нельзя совсем, но дело в пропущенном await: строка await book.addOrder() компилируется.
Главное
- Гонка данных — одновременное обращение двух частей программы к одной переменной, когда хотя бы одна её меняет; изменения при этом могут теряться.
- Актор — ссылочный тип, который обслуживает одну задачу за раз; его объявляют словом
actor. - Снаружи метод актора вызывают и свойство
varчитают черезawait, а внутри актора — напрямую. - Swift 6 ищет гонки данных при компиляции: вызов актора снаружи без
awaitне скомпилируется.