Перейти к содержанию

6. Actor isolation

Из прошлых статей можно сделать вывод, что Actor является потокобезопасным контейнером, который изолирует свой стейт и предоставляет доступ к нему только 1 задаче за одну единицу времени. Глобальный актор - делает тоже самое, только он шарит изоляцию между всеми всеми вызовами.

isolation

Синхронные методы, находящиеся внутри Актора изолированы им, обращение к таким методам происходит через await. Пример ниже:

actor LocalCache<T> {

    private var value: T?
    let lifetime: TimeInterval

    var cacheValue: T? {
        value
    }

    init(value: T, lifetime: TimeInterval) {
        self.value = value
        self.lifetime = lifetime
    }

    func setNewCacheValue(_ newValue: T?) {
        self.value = newValue
    }

    func dropCacheValue() {
        self.value = nil
    }
}

let cache = LocalCache(value: 2, lifetime: 1) 

Task {
    await cache.setNewCacheValue(5)
    await cache.dropCacheValue()
    let lifeTime = await cache.lifetime // кажется изоляция здесь лишняя
}

В примере выше изоляция необходима для изменяемых пропертей. Но вот для получения времени жизни кеша(cache.lifetime) - изоляция кажется излишней. Потому что cache.lifetime - неизменяемое проперти. Оно не будет изменяться на протяжении жизни всего эктора. Но как убрать эту лишнюю изоляцию?

nonisolated

Снять изоляцию с неизменяемых переменных

Если в экторе нам не требуется изоляция для какого-нибудь метода/переменной, то мы можем написать ключевое слово nonisolated и тогда изоляцию будет снята с переменной/функции

actor LocalCache<T> {
    ....

    nonisolated let lifetime: TimeInterval

    ...
}

let cache = LocalCache(value: 2, lifetime: 1) 

Task {
    let lifeTime = cache.lifetime // Теперь писать await не нужно
}

Протоколы

Если нам потребуется сделать наследование эктора от протокола, то нам также потребуется использование nonisolated

protocol AnyService {
    func setNewValue(_ newValue: Int)
}

actor Service: AnyService {

    // ЗДЕСЬ БУДЕТ ОШИБКА КОМПИЛЯЦИИ
    func setNewValue(_ newValue: Int) {
    }
}
Это происходит потому, что методы внутри эктора изолированы и для внешнего использования - они являются асинхронными, а метод в протоколе синхронный

Вариант 1

protocol AnyService {
    func setNewValue(_ newValue: Int)
}

actor Service: AnyService {

    /// Убрали изоляцию и метод стал синхронным - соответствует протоколу
    nonisolated func setNewValue(_ newValue: Int) { ... }
}

Вариант 2

protocol AnyService {
    func setNewValue(_ newValue: Int) async
}

actor Service: AnyService {

    /// Убрали изоляцию и метод стал синхронным - соответствует протоколу
    func setNewValue(_ newValue: Int) async { ... }
}

На начальном этапе для дебага и общего понимания хочется увидеть есть ли какая-нибудь изоляция у метода -- для этого можно использовать макрос #isolation