WWDC Quick Look 💓 By SwiftGGTeam
Eliminate data races using Swift Concurrency

Eliminate data races using Swift Concurrency

元の動画を見る

ハイライト

Swift Concurrency は、タスク分離、送信可能プロトコル、アクターの 3 つのメカニズムを通じてデータ競合を排除します。 Doug は、「海の船」のたとえを使用して同時実行における値型と参照型の違いを説明し、コンパイラーがタスク境界とアクター境界で送信可能コンプライアンスをチェックする方法を示し、アトミック性、メインスレッド分離、厳密な同時実行チェックモードなどの高度なトピックについて説明します。

主要内容

Swift 5.5 では、同時実行コードを簡単に作成できるように、async/await、タスク、およびアクターが導入されています。このセッションでは、これらのメカニズムがどのように連携してデータ競合を排除し、アプリのアーキテクチャに与える影響について概要を説明します。 (00:10)

ダグはシーン全体を通して、次のような例えを使いました。「タスクは、それぞれが独立して航行する海上の船です。 Actor は海上の島であり、独自のステータスを持ち、一度に 1 隻の船しか受け取ることができません。 Sendable は、船と船の間、船と島の間で渡される物品が安全であることを確認するための税関検査です。 (01:17)

値の型 (構造体、列挙型) は転送中にコピーされ、それぞれに独立したデータのコピーがあるため、当然ながら同時共有に適しています。参照型(クラス)は参照を転送します。複数のタスクが同じデータを同時に変更すると、データの競合が発生します。 Swift は、Sendable プロトコルを使用して、分離されたドメイン間でどの型を安全に渡すことができるかをマークします。値の型は (メンバーがすべて Sendable である限り) 自動的に Sendable に準拠し、参照型は不変であるか独自の同期を実行する場合にのみ準拠できます。 (02:31)

アクターは、可変状態を共有する安全な方法です。 Actor のすべてのプロパティとメソッドはデフォルトで Actor 内に分離されており、外部アクセスは経由する必要があります。await。アクター上で一度に実行できるタスクは 1 つだけなので、データ競合が排除されます。アクターの参照は「マップ」を取得するだけであり、状態への実際のアクセスには分離チェックに合格する必要があるため、アクター自体は暗黙的に Sendable に準拠します。 (10:28)

メイン スレッドはメイン アクタに対応し、すべての UI 操作はメイン アクタ上で実行する必要があります。 Main Actor はアクターでもあるため、一度に 1 つのタスクしか実行できません。長時間占有されている場合、UI がフリーズします。正しいアーキテクチャは、メイン アクターにビューとコントローラーを配置し、ビジネス ロジックを他のアクターまたは通常のタスクに分離することです。 (16:04)

原子性ももう 1 つの重要なトピックです。アクターは同時に 1 つのタスクのみが実行されることを保証しますが、awaitアクタが他のタスクを実行できる一時停止ポイントです。二人の間の場合awaitそれらの間で状態の仮定が行われ、高レベルのデータ競合が発生する可能性があります (状態は矛盾していますが、データは破損していません)。解決策は、アトミック性を必要とする操作をアクター内の同期メソッドとして作成することです。 (19:08)

Swift 5.7 では厳密な同時実行チェック モードが導入されており、これは 3 つのレベルに分かれています: Minimal (Sendable が明示的にマークされている場所のみをチェック)、ターゲット (同時実行機能を採用しているコードをチェック)、および Complete (すべてのコードをチェック、Swift 6 セマンティクスに近い)。を使用して、段階的に有効にすることをお勧めします。@preconcurrency import外部モジュールの警告を一時的に抑制します。 (24:36)

詳細

タスクの分離と送信可能プロトコル

(01:17) タスクは、同時実行の作業の基本単位です。各タスクには独自のリソースがあり、独立して実行されます。ただし、タスクは相互に通信する必要があり、これにはデータ共有が伴います。

値の型は渡されるときにコピーされます。

enum Ripeness {
    case hard
    case perfect
    case mushy(daysPast: Int)
}

struct Pineapple {
    var weight: Double
    var ripeness: Ripeness
    
    mutating func ripen() async { ... }
    mutating func slice() -> Int { ... }
}

キーポイント:

  • Pineapple構造体であり、渡されたときにコピーされます
  • 各タスクは独自のコピーを取得し、変更は相互に影響しません。
  • Swift コンパイラは自動的に推論しますPineapple: Sendable、すべてのメンバーが送信可能であるため

参照型は参照を渡します。

final class Chicken {
    let name: String
    var currentHunger: HungerLevel
    
    func feed() { ... }
    func play() { ... }
    func produce() -> Egg { ... }
}

キーポイント:

  • Chickenクラスなので、渡すときに参照をコピーします
  • 2 つのタスクが同じオブジェクトを参照しており、同時に変更するとデータの競合が発生します。
  • Chicken可変状態であるため、Sendable に準拠できません

Sendable プロトコルは、分離されたドメイン間で安全に渡すことができる型をマークするために使用されます。

protocol Sendable { }

struct Pineapple: Sendable { ... } // 自动符合
class Chicken: Sendable { } // 编译错误:未同步的引用类型

コンパイラはタスク境界で送信可能かどうかをチェックします。

// 编译错误:Chicken 不是 Sendable
let petAdoption = Task {
    let chickens = await hatchNewFlock()
    return chickens.randomElement()!
}
let pet = await petAdoption.value

キーポイント:

  • Taskの一般的なパラメータSuccess: Sendable戻り値は送信可能でなければなりません
  • この制約はタスクの定義から来ています。struct Task<Success: Sendable, Failure: Error>
  • 値が分離されたドメイン間で渡される場合は、送信可能な制約を汎用パラメーターにも使用する必要があります。

コレクション型の送信可能オブジェクトは、条件付き一貫性を介して伝播します。

// 符合 Sendable:成员都是 Sendable
struct Crate: Sendable {
    var pineapples: [Pineapple]
}

// 编译错误:Chicken 不是 Sendable
struct Coop: Sendable {
    var flock: [Chicken]
}

クラスを送信可能にするには、次の厳しい条件を満たす必要があります。

// 编译错误:currentHunger 是可变的
final class Chicken: Sendable {
    let name: String
    var currentHunger: HungerLevel
}

// 只有不可变成员的 final class 可以符合 Sendable
final class Egg: Sendable {
    let color: EggColor
    let weight: Double
}

独自の内部同期を行う参照型の場合は、次のように使用できます。@unchecked Sendableただし、次のことに注意してください。

class ConcurrentCache<Key: Hashable & Sendable, Value: Sendable>: @unchecked Sendable {
    var lock: NSLock
    var storage: [Key: Value]
}

キーポイント:

  • @unchecked Sendableコンパイラチェックを無効にする
  • 正しい同期が実際に行われた場合にのみ使用されます。
  • 悪用すると、Swift のデータ レースの安全性保証が破られる可能性があります

タスク作成時のクロージャーにも送信可能チェックがあります。

let lily = Chicken(name: "Lily")
Task.detached { @Sendable in
    lily.feed() // 编译错误:捕获了非 Sendable 的 lily
}

キーポイント:

  • @Sendableクロージャでは、キャプチャされたすべての値が送信可能であることが必要です
  • Task.detached操作パラメータのタイプは次のとおりです。@Sendable閉鎖
  • これにより、新しいタスクでデータ競合が発生しないことが保証されます。

アクターの隔離と状態の保護

(10:28) アクターは、変更可能な状態を安全に共有する方法を提供します。

actor Island {
    var flock: [Chicken]
    var food: [Pineapple]

    func advanceTime()
}

キーポイント:

  • アクターのインスタンス プロパティとインスタンス メソッドは、デフォルトでアクター上で分離されます。
  • 外部アクセスは経由する必要がありますawait、潜在的なハングポイントです
  • アクター上で同時に実行できるタスクは 1 つだけです
  • アクターは暗黙的に Sendable に準拠します

Actor メソッドを呼び出します。

func nextRound(islands: [Island]) async {
    for island in islands {
        await island.advanceTime()
    }
}

キーポイント:

  • それぞれawait island.advanceTime()それはすべて「島への上陸」です
  • 島にいる間、アクターのすべての状態にアクセスできます
  • 島を出た後(待機終了)、他のタスクが島に上陸できるようになります

送信不可能なデータはタスクとアクター間で共有できません。

// 两个都编译错误
await myIsland.addToFlock(myChicken) // 把非 Sendable 的 chicken 传给 Actor
myChicken = await myIsland.adoptPet() // 从 Actor 取出非 Sendable 的 chicken

アクター分離コードスコープ:

actor Island {
    var flock: [Chicken]
    var food: [Pineapple]

    func advanceTime() {
        // 闭包不标记 @Sendable,继承 Actor 隔离
        let totalSlices = food.indices.reduce(0) { (total, nextIndex) in
            total + food[nextIndex].slice()
        }

        // Task 继承创建时的 Actor 隔离
        Task {
            flock.map(Chicken.produce)
        }

        // Task.detached 不继承 Actor 隔离
        Task.detached {
            let ripePineapples = await food.filter { $0.ripeness == .perfect }
            print("There are \(ripePineapples.count) ripe pineapples on the island")
        }
    }
}

キーポイント:

  • いいえ@Sendableクロージャはアクター メソッド内でアクターの分離を継承します。
  • Task {}作成時にアクター分離コンテキストを継承する
  • Task.detached {}完全に独立しており、アクターの分離を継承しません。

使用nonisolatedアクターの孤立から明示的に抜け出します。

extension Island {
    nonisolated func meetTheFlock() async {
        let flockNames = await flock.map { $0.name }
        print("Meet our fabulous flock: \(flockNames)")
    }
}

キーポイント:

  • nonisolatedメソッドをアクターの外部で実行させます
  • アクター状態へのアクセスには依然として必要ですawait
  • アクター状態への頻繁なアクセスを必要としない補助メソッドに適しています

主要なアクターとアプリのアーキテクチャ

(16:04) Main Actor は、すべての UI 操作を実行する必要があるメイン スレッドを表します。

@MainActor func updateView() { ... }

Task { @MainActor in
    view.selectedChicken = lily
}

nonisolated func computeAndUpdate() async {
    computeNewValues()
    await updateView()
}

キーポイント:

  • @MainActor関数、クロージャ、または型をマークできます
  • メイン アクター以外のコンテキストから呼び出す場合は必須await
  • Main Actor を長時間使用すると、UI がフリーズします。

メイン アクターはタイプ全体をマークすることもできます。

@MainActor
class ChickenValley: Sendable {
    var flock: [Chicken]
    var food: [Pineapple]

    func advanceTime() {
        for chicken in flock {
            chicken.eat(from: &food)
        }
    }
}

キーポイント:

  • タイプのすべてのプロパティとメソッドはデフォルトで Main Actor に分離されます
  • データが分離されているため、メイン アクター タイプの参照は送信可能です
  • メインスレッドで実行する必要があるビュー、ビューコントローラーなどに適しています。

アプリのアーキテクチャへの影響:

  • Main Actor のビューとビュー コントローラー
  • ビジネス ロジックを他のアクターまたは通常のタスクに分離する
  • オンデマンドでメイン アクターと他のアクター間のタスク シャトル

原子性と高レベルのデータ競合

(19:08) アクターは低レベルのデータ競合 (データ破損) を排除しますが、高レベルのデータ競合 (不整合な状態) が依然として発生する可能性があります。

// 有问题:两个 await 之间状态可能变化
func deposit(pineapples: [Pineapple], onto island: Island) async {
    var food = await island.food // await 1:拿到副本
    food += pineapples // 在本地修改
    await island.food = food // await 2:写回去,但期间可能已被修改
}

キーポイント:

  • awaitアクターは合間に他のタスクを実行できます
  • 他のタスクが変更された場合food、最後の割り当てによってそれらの変更が上書きされます。
  • これは高レベルのデータ競合です。データの破損はありませんが、ロジック エラーが発生します。

正しいアプローチ: アクターの同期メソッドとして操作を記述します。

extension Island {
    func deposit(pineapples: [Pineapple]) {
        var food = self.food
        food += pineapples
        self.food = food
    }
}

キーポイント:

  • 同期メソッドはアクター上で不可分に実行されます。
  • いいえawait、他のタスクは挿入されません
  • アクターを設計するときは、アトミック性を必要とする操作を同期メソッドに作成します。

厳密な同時実行チェックモード

(24:36) Swift 5.7 では、3 つのレベルの厳密な同時実行チェックが導入されています。

最小 (デフォルト):

  • Sendable が明示的にマークされている場所のみをチェックします
  • Swift 5.5/5.6 と同様の動作
import FarmAnimals
struct Coop: Sendable {
    var flock: [Chicken] // 警告:Chicken 不是 Sendable
}

Targeted:

  • 同時実行機能(async/await、Task、Actor)を採用したコードを確認する
  • 同時実行性を使用しないコードとの互換性を維持する
@preconcurrency import FarmAnimals

func visit(coop: Coop) async {
    guard let favorite = coop.flock.randomElement() else {
        return
    }
    Task {
        favorite.play()
    }
}

キーポイント:

  • @preconcurrency import外部モジュールの送信可能警告を一時的に抑制する
  • 外部モジュールが更新されると、コンパイラは再チェックを行います
  • 段階的な移行シナリオに適しています

Complete:

  • モジュール内のすべてのコードをチェックします
  • Swift 6 の予想されるセマンティクスに近い
import FarmAnimals

func doWork(_ body: @Sendable @escaping () -> Void) {
    DispatchQueue.global().async {
        body()
    }
}

func visit(friend: Chicken) {
    doWork {
        friend.play() // 警告:捕获了非 Sendable 的 Chicken
    }
}

キーポイント:

  • 完全モードでは、潜在的なデータ競合をすべて検出します
  • 段階的に有効にすることをお勧めします。最初にターゲットを指定し、次に完了します。
  • 使用@preconcurrencyまだ更新されていない依存モジュールを処理する

重要ポイント

  • 対処方法: プロジェクト内のクラスを確認し、可変状態を struct または Actor に変更します。 これを行う価値がある理由: Doug は、パイナップル (構造体) とチキン (クラス) の比較を使用して、値の型は渡されるときにコピーされるため、当然ながら同時実行に適していることを説明しました。クラスへの共有参照はデータ競合の原因になります。 Swift コンパイラーは、struct/enum に対して Sendable を自動的に推論しますが、クラスには厳密な条件が必要です。 開始方法: プロジェクト内のデータ モデルをループして、「この型には参照セマンティクスが本当に必要か?」と自問してください。そうでない場合は、構造体に変更します。可変状態を共有する必要がある場合は、それを Actor に変更します。

  • 内容: ターゲットを絞った厳密な同時実行チェックを有効にし、送信可能な警告を修正します。 実行する価値がある理由: Swift 5.7 のターゲット モードは、すでに同時実行機能を採用しているコードのみをチェックし、既存のコードを壊すことはありません。これらの警告を修正すると、潜在的なデータ競合がほとんどなくなります。 Doug 氏は、Swift 6 に備えるために、完全チェックに徐々に移行することをお勧めします。 開始方法: ビルド設定で「厳密な同時実行チェック」を「ターゲット」に変更し、プロジェクトをコンパイルし、Sendable 関連の警告を 1 つずつ修正します。外部モジュールで使用する場合@preconcurrency import一時的に抑制されました。

  • 対処方法: アクター内の非同期メソッドを確認し、アトミック性が必要な操作を同期メソッドに変更します。 実行する価値がある理由: Doug の入金例、2awaitアクターのステータスはその間に変化する可能性があります。マルチステップ操作をアクター内の同期メソッドにカプセル化すると、アトミック性を確保できます。 開始方法: すべてのアクターのパブリック メソッドを調べて、複数のアクターが存在するかどうかを確認します。await方法。 「これら 2 つの待機の間に他のタスクが挿入された場合、状態は依然として正しいでしょうか?」と自問してください。よくわからない場合は、ロジックを同期メソッドに変更してください。

関連セッション

コメント

GitHub Issues · utterances