Является ли ключевое слово let потокобезопасным в Swift?

Ответ

Да, но с важным нюансом.

Ключевое слово let объявляет иммутабельную (неизменяемую) константу. Поскольку значение не может быть изменено после инициализации, чтение этой константы из любого количества потоков абсолютно безопасно — нет риска гонки данных (race condition).

Однако, если let ссылается на экземпляр ссылочного типа (например, класса), потокобезопасность касается только самой ссылки, но не внутреннего состояния объекта. Свойства этого объекта, объявленные как var, могут изменяться, что требует синхронизации.

Примеры:

// 1. Value-типы (безопасно):
let safeInteger: Int = 42
let safeArray: [String] = ["A", "B", "C"]
// Чтение safeInteger или safeArray из любого потока безопасно.

// 2. Reference-типы (требует осторожности):
let myObject = MyClass() // Ссылка иммутабельна, но...
class MyClass {
    var mutableProperty = 0 // ...это свойство можно менять!
}
// Одновременная запись в mutableProperty из разных потоков вызовет гонку данных.

Вывод: let гарантирует потокобезопасность для значения value-типа и для самой ссылки reference-типа. Для обеспечения потокобезопасности изменяемого состояния объекта используйте акторы (actor), очереди (DispatchQueue) или другие механизмы синхронизации.

Ответ 18+ 🔞

А, ну вот, классика! Подходишь к этому let, как к священной корове, думаешь — ну всё, железобетонная безопасность, можно расслабиться и не бздеть. Ан нет, сука, тут как раз тот самый случай, где дьявол в деталях прячется, как хитрая жопа в кустах.

Так, слушай сюда, разжую.

Да, let — это константа. Раз присвоил — и хуй с горы, менять нельзя. И вот этот самый факт, что значение намертво зафиксировано, делает чтение из любого количества потоков абсолютно безопасным. Никаких гонок, потому что менять-то нечего, всем потокам одно и то же светит. Это как если бы у тебя на столе лежала бумажка с числом 42, и двадцать человек одновременно могли бы на неё смотреть — ну и что? Ничего, всем 42.

НО! Вот это самое «но» размером с овердохуище.

Всё это волшебство работает на ура, пока мы говорим про value-типы (структуры, энамы, кортежи, простые типы вроде Int). Там значение — оно и есть значение, оно в константе и сидит.

let священноеЧисло: Int = 42 // Всё. Забетонировано. Можно хоть из ста потоков читать.
let несокрушимыйМассив: [String] = ["А", "Б", "В"] // Тоже value-тип. Безопасно.

А теперь представь, что let — это не сейф, а поводок. И этот поводок привязан не к кирпичу (значению), а к живой, ебучей, непредсказуемой собаке — к экземпляру класса (reference-типу).

let поводок = Собака() // Сам поводок не порвётся, он `let`.
class Собака {
    var настроение = "Спокойное" // А вот внутреннее состояние собаки — запросто!
}

Понимаешь, в чём подвох? let гарантирует, что ссылка (этот самый поводок) неизменна. Ты не сможешь сказать поводок = другаяСобака(). Но он ни хуя не мешает тебе менять внутренние свойства той самой собаки, на которую ссылка указывает! Свойство-то var объявлено.

И вот если два потока одновременно начнут эту самую собаку дёргать — один пытаться настроение = "Злое" записать, а другой в это же время читать — вот тебе, блядь, и гонка данных (race condition) во всей красе. Пиздец и хаос. Один поток видит одно состояние, другой — другое, программа ведёт себя как пьяный мартышлюшка.

Так что вывод, блядь, какой?

  1. Для value-типовlet это царь и бог, полная потокобезопасность при чтении.
  2. Для reference-типовlet защищает только саму ссылку (поводок), но не защищает ебучую собаку (изменяемое состояние объекта) на другом конце.

Поэтому, если у тебя let держит объект класса, а внутри у него что-то var и с этим что-то будут работать несколько потоков — включай голову, ёпта! Используй акторы (actor), очереди (DispatchQueue), замки (NSLock) — в общем, весь этот арсенал для синхронизации, чтобы твоя программа не превратилась в цирк с конями. Вот так вот, нахуй.