В чём разница между протоколом Sendable и гонками данных (data races) в Swift Concurrency?
Data race — рантайм-неопределённость при параллельном доступе к памяти; Sendable — статическая гарантия компилятора Swift 6, запрещающая передачу небезопасных типов между акторами. Sendable предотвращает гонки на этапе компиляции, но @unchecked Sendable требует ручной синхронизации.
Sendable и гонки данных: в чём разница
Это два уровня одной проблемы. Гонка данных (data race) — это undefined behavior в рантайме, когда два потока одновременно обращаются к одному участку памяти и хотя бы один из них пишет. Sendable — это статическая гарантия на уровне системы типов Swift Concurrency, призванная предотвратить гонки ещё до выполнения программы.
Что такое data race
Классическая гонка данных: два потока модифицируют общий объект без синхронизации. В Swift это возможно при использовании GCD или POSIX-потоков с ссылочными типами.
import Foundation
// НЕБЕЗОПАСНО: data race при параллельном доступе
class Counter {
var value = 0
func increment() { value += 1 }
}
let counter = Counter()
let group = DispatchGroup()
for _ in 0..<1000 {
DispatchQueue.global().async(group: group) {
counter.increment() // data race!
}
}
group.wait()
print(counter.value) // непредсказуемый результат: < 1000
Sendable как статическая защита
Протокол Sendable позволяет компилятору Swift 6 статически запретить передачу небезопасных типов через границы параллельного выполнения. Это не устраняет все гонки, но ликвидирует целый класс небезопасного кода на этапе компиляции.
// Swift 6: компилятор отклоняет передачу non-Sendable типа
class MutableConfig {
var maxConnections = 10
}
actor NetworkManager {
func configure(with config: MutableConfig) {
// Ошибка: 'MutableConfig' не соответствует 'Sendable'
// Передача ссылочного типа небезопасна
}
}
Правильное решение: actor или Sendable value type
// Вариант 1: value type — автоматически Sendable
struct Config: Sendable {
let maxConnections: Int
let timeout: TimeInterval
}
// Вариант 2: actor изолирует состояние
actor SafeCounter {
private var value = 0
func increment() {
value += 1
}
func current() -> Int { value }
}
Task {
let counter = SafeCounter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<1000 {
group.addTask {
await counter.increment()
}
}
}
print(await counter.current()) // гарантированно 1000
}
Sendable не означает «без гонок» автоматически
@unchecked Sendable позволяет обойти проверки компилятора. Разработчик декларирует безопасность вручную — и несёт ответственность за корректную синхронизацию.
import os
final class LockProtectedState: @unchecked Sendable {
private let lock = OSAllocatedUnfairLock()
private var _value = 0
func increment() {
lock.withLock { _value += 1 }
}
var value: Int {
lock.withLock { _value }
}
}
// Это Sendable и потокобезопасно, но только если lock применяется корректно
Swift 6 strict concurrency: что изменилось
- Компилятор повышает предупреждения до ошибок при нарушении Sendable-границ.
- Все замыкания в
Task { }должны быть@Sendable. - Передача
non-Sendableтипа между акторами — ошибка компиляции. - Migrate поэтапно:
-strict-concurrency=minimal→targeted→complete.
Подводные камни
Sendable— необходимое, но недостаточное условие отсутствия гонок:@unchecked Sendableбез правильной синхронизации даст гонку в рантайме.- Actor не устраняет гонки полностью при reentrancy: между двумя
await-вызовами состояние актора может быть изменено другой задачей. - Глобальные переменные (
var) — потенциальный источник гонок; в Swift 6 они требуютnonisolated(unsafe)или перехода в actor. - Проверка
Sendableне покрывает Objective-C типы — они принимаются как@unchecked Sendableавтоматически. - Reentrancy actor: если метод актора выполняет
await, другие задачи могут войти в этот же актор в промежутке. - Использование
Task.detachedснимает наследование actor-контекста, что может неожиданно нарушить изоляцию. - Инструмент Thread Sanitizer (TSan) детектирует гонки в рантайме, но не заменяет статические проверки Swift 6.
- Миграция больших Objective-C кодовых баз на Swift Concurrency требует аудита всех точек передачи данных между потоками.
Common mistakes
- Сводить «протоколом
Sendableи гонками данных (data races) в Swift Concurrency» к синтаксису и не объяснять модель памяти. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swift-24.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «протоколом
Sendableи гонками данных (data races) в Swift Concurrency» и подтверждает ее корректным примером. - Умеет связать ответ с система типов, тестированием и отладкой на устройстве.
- Называет ограничения подхода swift-24, включая производительность, память и сопровождение.