Ответ
Требуется синхронизация доступа. Без нее возникает состояние гонки (race condition), приводящее к неопределенному поведению и крашам.
1. Serial DispatchQueue (базовый подход):
let serialQueue = DispatchQueue(label: "com.app.dataQueue", qos: .userInitiated)
private var _counter = 0
var counter: Int {
get { serialQueue.sync { _counter } }
set { serialQueue.async(flags: .barrier) { self._counter = newValue } }
}
// Использование
serialQueue.async {
self._counter += 1
print("Counter: (self._counter)")
}
2. Actor (Swift 5.5+, Concurrency):
actor BankAccount {
private var balance: Decimal = 0
func deposit(amount: Decimal) {
balance += amount
}
func withdraw(amount: Decimal) async throws {
guard balance >= amount else { throw InsufficientFundsError() }
balance -= amount
}
func currentBalance() async -> Decimal {
return balance
}
}
// Использование
let account = BankAccount()
await account.deposit(amount: 1000)
let balance = await account.currentBalance()
3. @Atomic property wrapper (ручная реализация):
import os.lock
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private let lock = os_unfair_lock()
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
return value
}
set {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
value = newValue
}
}
}
// Использование
class SharedData {
@Atomic var counter = 0
@Atomic var configuration: [String: Any] = [:]
}
4. NSLock / NSCondition (Objective-C совместимость):
class ThreadSafeArray<T> {
private var storage: [T] = []
private let lock = NSLock()
func append(_ element: T) {
lock.lock()
defer { lock.unlock() }
storage.append(element)
}
var count: Int {
lock.lock()
defer { lock.unlock() }
return storage.count
}
}
Рекомендации по выбору:
- Actor — для нового кода с async/await
- Serial Queue — для GCD-кода, простые случаи
- @Atomic — для отдельных свойств
- NSLock — при работе с Objective-C кодом
Диагностика: Включите Thread Sanitizer (TSan) в схеме для обнаружения гонок данных.
Ответ 18+ 🔞
Смотри, вот тебе про синхронизацию, как есть. Ситуация, блядь, классическая: у тебя несколько потоков лезут в одну переменную, как мухи на говно, и начинается пиздец — состояние гонки, или race condition. Программа начинает вести себя как пьяный ёжик в тумане: то работает, то вылетает, то значения хуй пойми какие. Короче, доверия к такому коду — ноль ебать.
1. Serial DispatchQueue (дедовский способ, но рабочий)
let serialQueue = DispatchQueue(label: "com.app.dataQueue", qos: .userInitiated)
private var _counter = 0
var counter: Int {
get { serialQueue.sync { _counter } }
set { serialQueue.async(flags: .barrier) { self._counter = newValue } }
}
// Использование
serialQueue.async {
self._counter += 1
print("Counter: (self._counter)")
}
Суть проста, как три копейки: создаёшь очередь, которая выполняет задачи строго по одной. Всё, что связано с твоей переменной, кидаешь в эту очередь. sync — значит, ждём результата, async — отправил и пошёл дальше. Флаг .barrier для записи — это чтобы пока запись идёт, чтение ждало. Без него можно, но с ним надёжнее, как ремень безопасности.
2. Actor (модный, молодёжный, Swift 5.5+)
actor BankAccount {
private var balance: Decimal = 0
func deposit(amount: Decimal) {
balance += amount
}
func withdraw(amount: Decimal) async throws {
guard balance >= amount else { throw InsufficientFundsError() }
balance -= amount
}
func currentBalance() async -> Decimal {
return balance
}
}
// Использование
let account = BankAccount()
await account.deposit(amount: 1000)
let balance = await account.currentBalance()
Вот это, блядь, красота! Компилятор сам следит, чтобы к свойствам актора обращались только через await. Всё изолировано, синхронизировано, и тебе даже думать об этом не надо. Чистая магия, ёпта! Если пишешь на новой конкуренси — это твой выбор.
3. @Atomic property wrapper (ручная сборка, для ценителей)
import os.lock
@propertyWrapper
struct Atomic<Value> {
private var value: Value
private let lock = os_unfair_lock()
init(wrappedValue: Value) {
self.value = wrappedValue
}
var wrappedValue: Value {
get {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
return value
}
set {
os_unfair_lock_lock(&lock)
defer { os_unfair_lock_unlock(&lock) }
value = newValue
}
}
}
// Использование
class SharedData {
@Atomic var counter = 0
@Atomic var configuration: [String: Any] = [:]
}
Тут мы берем низкоуровневый замок os_unfair_lock. Он быстрый, легковесный. Обернул свойство — и всё, оно теперь атомарное. Читаешь, пишешь — всё под замком. defer — это чтобы даже если в середине вылетит исключение, замок всё равно отпустили. Удобная штука для отдельных переменных.
4. NSLock / NSCondition (старая гвардия, для совместимости)
class ThreadSafeArray<T> {
private var storage: [T] = []
private let lock = NSLock()
func append(_ element: T) {
lock.lock()
defer { lock.unlock() }
storage.append(element)
}
var count: Int {
lock.lock()
defer { lock.unlock() }
return storage.count
}
}
Старый добрый NSLock. Если работаешь в смешанном проекте или просто привык — почему бы и нет. Главное — не забудь его отпустить, а то deadlock получишь и будешь сидеть, как дурак, и думать, почему всё зависло.
Так что же выбрать, ёпта?
- Actor — если твой код уже на новой конкуренси (
async/await). Это путь джедая. - Serial Queue — если проект на GCD и нужно что-то простое и понятное.
- @Atomic — если нужно защитить парочку конкретных свойств, а не весь объект.
- NSLock — если вокруг тебя одни Objective-C файлы и ты чувствуешь себя археологом.
И главный совет, нахуй: включи Thread Sanitizer (TSan) в настройках схемы! Он эти гонки данных находит, как собака наркотики. Запустил — и смотри, где у тебя потоки друг другу в тапки срут. Без него ты будешь как слепой котёнок — тыкаться носом и удивляться, почему опять краш.