2. Actor Reentrancy
Изучение данного блока предполагает предварительное знание синтаксиса языка Swift. Для успешного освоения этого материала, необходимо иметь базовое понимание синтаксиса языка Swift. Это включает в себя знание основных структур данных, операторов, циклов, функций, абстракций и других ключевых элементов языка. Без этих фундаментальных знаний будет сложно понять более сложные концепции и примеры, которые будут рассматриваться в данном блоке.
На первый взгляд кажется, что работа с Actor очень простая. Но на самом деле есть достаточно неприятные проблемы о которых необходимо знать. Первая из них - это высокоуровневые гонки (потокобезопасность остаётся).
Рассмотрим проблему на примере:
Есть человек(Person). У него есть друг и мнение человека(opinion). Есть две асинхронные функции - подумать о хорошем и о плохом - это позволяет зафиксировать своё мнение и рассказать его другу. Что будет выведено на экран в коде ниже?
actor Person {
let friend: Friend
var opinion: Judgment = .noIdea
init(friend: Friend) {
self.friend = friend
}
func thinkOfGood() async -> Decision {
opinion = .goodIdea // 1
await friend.tell(opinion) // 2
return opinion
}
func thinkOfBad() async -> Decision {
opinion = .badIdea // 3
await friend.tell(opinion) // 4
return opinion
}
}
// Запустим две задачи одновременно
async let goodThink = person.thinkOfGood()
async let badThink = person.thinkOfBad()
let (shouldBeGood, shouldBeBad) = await (goodThink, badThink)
print(shouldBeGood)
.goodIdea или .badIdea
Теория и объяснение¶
На самом деле такое поведение логично для Actor. Почему? Напомню, что вызов await - это точка приостановки исполнения когда, на данном этапе формируется continuation и в этот момент _Actor освобождается и он сразу же берёт в работу другую задачу._ Это и приводит к высокоуровневым гонкам. Обсудим пример выше по шагам
/// Предположим первым попал на исполнение метод thinkOfGood(), тогда:
opinion = .goodIdea // 1 - Меняем общее состояние await friend.tell(...)
await friend.tell(...) // 2 - После чего происходит приостановка
/// В данный момент Actor берёт на исполнение другую задачу thinkOfBad()
opinion = .badIdea // 3 - СНОВА Меняем общее состояние
await friend.tell(...) // 4 - После чего происходит приостановка
/// и теперь возвращается и продолжает своё исполнение thinkOfGood()
/// ей остаётся вернуть общий стейт и она возвращает неверный
Как с этим бороться?¶
- Изменять стейт после вызова всех асинхронных функций
- Реорганизовывать логику работы функций и освобождать Actor от async функций
- Добавление в код Task
Мы хотим построить хранилище данных, которое бы имело кеш и отдавало из него данные. В случае отсутствия данных мы бы шли в базу и запрашивали их (предположим, что поход в базу для нас является очень тяжелой операций).
Посмотрите внимательно на код и ответьте, что не так в этом коде? И если есть ошибки, то как их исправить?
actor ActivitiesStorage {
/// Изолированный кеш
var cache = [UUID: Data?]()
func retrieveHeavyData(for id: UUID) async -> Data? {
if let data = cache[id] {
return data
}
let data = await requestDataFromDatabase(for: id)
cache[id] = data
return data
}
private func requestDataFromDatabase(for id: UUID) async -> Data? {
print("Выполняется загрузка из базы данных!")
try? await Task.sleep(for: .seconds(7))
return nil
}
}
// Запуск кода
let id = UUID()
let storage = ActivitiesStorage()
Task {
let data = await storage.retrieveHeavyData(for: id)
}
Task {
let data = await storage.retrieveHeavyData(for: id)
}
Самая большая ошибка здесь заключается в том, что если мы запустим 2 функции одновременно, то есть большая вероятность сходить в базу данных 2 раза. То есть мы зашли в функцию - проверили кеш - там ничего - освободили Actor и ушли в базу. Аналогично при втором заходе в функцию. И так может продолжаться, до тех пор пока мы не положим значение в кеш. Как же нам необходимо изменить код?
actor ActivitiesStorage {
/// Изолированный кеш
var cache = [UUID: Task<Data?, Never>]()
func retrieveHeavyData(for id: UUID) async -> Data? {
/// Проверяем в кеше наличие задачи - если она есть
/// то мы либо возвращаем значение, которое лежит в ней,
/// либо ждём пока завершится выполнение задачи
if let dataTask = cache[id] {
return await dataTask.value
}
let dataTask = Task {
await requestDataFromDatabase(for: id)
}
cache[id] = dataTask
/// После чего уже вызываем await тем самым особождая эктор
return await dataTask.value
}
private func requestDataFromDatabase(for id: UUID) async -> Data? {
print("Выполняется загрузка из базы данных!")
try? await Task.sleep(for: .seconds(7))
return nil
}
}
Мы с вами нашли прекрасное применение Task - она помогла нам решить проблему с Actor Reentrancy и сделать код безопасным и лаконичным.
Итого¶
- Синхронные функции на Actor выполняются синхронно, в этих функциях состояние корректное
- async функции на actor могут освобождать его от выполнения текущей задачи (при обнаружении ожидания), что дает возможность для запуска других задач на акторе - по возвращению стейт уже может быть другм.