Irbisa · Шпаргалка

Строгая конкурентность Swift 6

Домены изоляции, Sendable, глобальные акторы, region isolation, sending, миграция на Swift 6

Senior-разработчик · 19 вопросов

  1. 01

    Что проверяет strict concurrency в Swift 6 и чем языковой режим отличается от версии компилятора?

    Она на этапе компиляции доказывает, что изменяемое состояние не достижимо из двух доменов изоляции одновременно: значение, пересекающее границу, либо Sendable, либо доказуемо передано.

    // swift-tools-version: 6.0// Tools 6.0 = Swift 6 compiler; targets default to language mode 6.import PackageDescription let package = Package(  name: "App",…
  2. 02

    Что такое домен изоляции в Swift и как значение может перейти из одного домена в другой?

    Доменом изоляции называют область, изменяемое состояние которой в каждый момент трогает одна задача: экземпляр актора, глобальный актор вроде @MainActor; nonisolated-код не принадлежит ни одному.

  3. 03

    Почему класс только с let-свойствами всё равно не Sendable и когда @unchecked Sendable будет честным решением?

    Для класса Sendable не выводится: конформанс пишете вы, и класс должен быть final, с неизменяемыми Sendable-свойствами и без суперкласса, кроме NSObject.

    import Foundationimport Synchronization // Rejected, one reason each:// class A: Sendable { let x = 1 }                       // not final// final class B: Sendable { var x = 1 }                 // mutable stored property…
  4. 04

    Swift 6 отклоняет оба объявления в Services. О чём говорит каждая ошибка и какие формы допустимы для разделяемого изменяемого состояния?

    final class Analytics {  var queue: [String] = []} enum Services {  static var currentUserID: String?     // (1)  static let analytics = Analytics()    // (2)}

    Строка (1) объявляет глобальную переменную, в которую может писать любой поток без сериализации.

  5. 05

    Что nonisolated(unsafe) обещает компилятору и когда это честный ответ, а не способ заглушить ошибку?

    Он выключает проверку конкурентности для одного объявления и обещает, что каждое обращение вы сериализуете сами; никакого лока и никакой защиты в рантайме он не добавляет.

    import Foundationimport Synchronization enum Metrics {  private static let lock = NSLock()  // Invariant: read and written only while `lock` is held.…
  6. 06

    Почему класс с Mutex может быть Sendable без @unchecked и какие у Mutex ограничения?

    Mutex<Value> (iOS 18+) представляет собой лок, который владеет защищаемым значением: значение достижимо только внутри withLock, а сам мьютекс Sendable, поэтому класс, который его хранит, конформит Sendable без @unchecked.

    import Synchronizationimport os // iOS 18+: the dictionary is reachable only through the lock.final class TokenCache: Sendable {  private let tokens = Mutex<[String: String]>([:])…
  7. 07

    Найдите баг: этот Sendable-класс статистики без ошибок компилируется в Swift 6, но под нагрузкой счётчики выходят меньше, чем было вызовов record.

    final class RequestStats: Sendable {  private let storage = Mutex<[String: Int]>([:])   var counts: [String: Int] {    get { storage.withLock { $0 } }    set { storage.withLock { $0 = newValue } }  }   func record(_ host: String) {    counts[host, default: 0] += 1  }   func reset() { counts = [:] }}

    counts[host, default: 0] += 1 проходит через геттер, а затем через сеттер, то есть берёт лок дважды: копирует словарь под одним локом, увеличивает копию без лока и записывает её обратно под другим.

  8. 08

    Что такое глобальный актор и когда стоит объявлять свой вместо @MainActor или обычного actor?

    Глобальный актор представляет собой один общий экземпляр актора, названный атрибутом, так что объявления из множества типов делят один домен изоляции; @MainActor служит встроенным примером.

    @globalActoractor StorageActor {  static let shared = StorageActor()} @StorageActor…
  9. 09

    Что означает nonisolated на хранимом свойстве, на синхронном и на async-методе и уводит ли он работу с актора?

    nonisolated выводит объявление из домена изоляции его актора: вызывающие обращаются к нему без await, а оно само не может трогать изолированное состояние.

    @MainActorfinal class Profile: Hashable {  nonisolated let id: UUID         // Sendable let: readable from any domain  var name: String                 // main-actor state   init(id: UUID, name: String) { self.id = id; self.name = name }…
  10. 10

    Скомпилируется ли этот код в языковом режиме Swift 6 и что изменится, если удалить строку с print?

    final class Report {            // not Sendable  var title = ""} actor Archive {  private var reports: [Report] = []  func store(_ r: Report) { reports.append(r) }} func archiveWeekly(_ archive: Archive) async {  let report = Report()  report.title = "Weekly"  await archive.store(report)  print(report.title)}

    В таком виде код не скомпилируется из-за ошибки «sending 'report' risks causing data races»: report читается после того, как его передали актору.

  11. 11

    Что может выразить параметр sending, чего не может @Sendable, и почему Task.init и addTask принимают sending-замыкания?

    @Sendable ограничивает захваты замыкания Sendable-значениями, потому что оно может выполняться где угодно и сколько угодно раз.

    final class Draft { var body = "" }      // not Sendable actor Outbox {  private var queued: [Draft] = []  func queue(_ d: Draft) { queued.append(d) }}…
  12. 12

    После включения Approachable Concurrency в таргете скролл подтормаживает, пока эта модель декодирует картинки. Почему и как исправить?

    @MainActorfinal class GalleryModel {  var thumbnails: [UIImage] = []   func load(_ files: [Data]) async {    for data in files {      if let image = await Self.decode(data) { thumbnails.append(image) }    }  }   nonisolated static func decode(_ data: Data) async -> UIImage? {    UIImage(data: data)?.preparingForDisplay()  }}

    С NonisolatedNonsendingByDefault (SE-0461, Swift 6.2) функция nonisolated async выполняется на акторе вызывающего, поэтому decode теперь работает на главном акторе и декодирует каждую картинку в главном потоке.

  13. 13

    Как перевести большое приложение со Swift 5 на языковой режим Swift 6, не останавливая работу над фичами?

    Таргет за таргетом, начиная с листьев графа зависимостей: в режиме Swift 5 включите полную проверку, чтобы каждое нарушение было предупреждением, доведите предупреждения до нуля, затем переключите этот таргет на Swift 6.

    // Package.swift: one target at a time, leaves first.let package = Package(  name: "App",  targets: [    .target(      name: "CoreModels",…
  14. 14

    Что меняют в таргете Approachable Concurrency и изоляция MainActor по умолчанию и почему их не стоит включать в общем фреймворке?

    Изоляция MainActor по умолчанию (SE-0466) делает каждое неаннотированное объявление @MainActor; Approachable Concurrency включает набор upcoming features, подходящих этой модели.

  15. 15

    Ваша @MainActor-вью-модель должна конформить Equatable, а компилятор отклоняет конформанс. Почему и какие есть варианты?

    Equatable.== объявлен как неизолированное требование: любой код может вызвать его синхронно из любого потока, а реализация на главном акторе этого обеспечить не может.

    @MainActorfinal class Model {  let id: Int  var rows: [String] = []  init(id: Int) { self.id = id }}…
  16. 16

    Что делает @preconcurrency на импорте и на конформансе протоколу и какой риск остаётся после того, как сборка позеленела?

    На импорте он отключает диагностики об отсутствующем Sendable у типов этого модуля; на конформансе позволяет изолированному типу выполнить неизолированное требование, меняя ошибку компиляции на падение в рантайме.

    @preconcurrency import LegacyPlayer   // stop diagnosing its missing Sendable @MainActorfinal class PlayerBox: NSObject {  private let player = LegacyPlayer()  private var finished = 0…
  17. 17

    Колбэк вендора по документации приходит в главном потоке, но метод nonisolated. Когда MainActor.assumeIsolated уместен и когда он падает?

    assumeIsolated в рантайме проверяет, что вы уже на главном акторе, а затем синхронно выполняет замыкание с изоляцией главного актора; поток он никогда не переключает.

    @MainActorfinal class Player {  private var progress: Double = 0   // SDK requirement is nonisolated; docs say "always called on the main thread".  nonisolated func vendorDidTick(_ value: Double) {…
  18. 18

    Objective-C SDK вендора вызывает ваш делегат на произвольных потоках. Как встроить его в модуль Swift 6 без @unchecked Sendable повсюду?

    Поставьте между SDK и своим кодом один маленький неизолированный объект: только его трогают потоки вендора, он не владеет изменяемым состоянием и пересылает каждое событие в AsyncStream, который потребляет изолированная модель.

    final class ScannerBridge: NSObject, VendorScannerDelegate, Sendable {  enum Event: Sendable { case found(String) }  let events: AsyncStream<Event>  private let continuation: AsyncStream<Event>.Continuation  override init() {    (events, continuation) = AsyncStream.makeStream(of: Event.self)…
  19. 19

    Таргет чисто компилируется в режиме Swift 6, но Thread Sanitizer находит гонку данных. Чего не видит компилятор и как найти гонку?

    Правы оба: Swift 6 доказывает, что незащищённое Swift-значение не пересекает границу изоляции в коде, который ему разрешили проверить, а TSan следит за реальными обращениями к памяти, включая код, который компилятор пропустил или не видел.