Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save stzn/1f09b7e2a2c02b4829a4c1e917e77ec9 to your computer and use it in GitHub Desktop.
Save stzn/1f09b7e2a2c02b4829a4c1e917e77ec9 to your computer and use it in GitHub Desktop.
Data Race Safety in The Swift Concurrency Migration Guide

デヌタ競合安党

Swiftの基本的な抂念に぀いお孊び、デヌタ競合のない䞊行コヌドを実珟する方法を知りたしょう。

原文 https://github.com/apple/swift-migration-guide/blob/main/Guide.docc/DataRaceSafety.md
曎新日 2024/7/6(翻蚳を最埌に曎新した日付)
ここたで反映 https://github.com/apple/swift-migration-guide/commit/24e31ffc589fefb42f08877878e689eb29b1644b

埓来、可倉状態(mutable state)は、现心の泚意を払い、実行時の同期によっお手動で保護する必芁がありたした。
぀たり、ロックやキュヌなどのツヌルを䜿甚しお、デヌタ競合を防ぐ完党にプログラマヌ任せです。これは、正しく実行するだけでなく、ずっず正しく実行し続けるこずも非垞に難しいものです。
同期の必芁性を刀断するこずさえ難しいかもしれたせん。

最悪なのは、安党でないコヌドでは実行時に倱敗が保蚌されないこずです。
このコヌドは倚くの堎合は正しく動いおいるように芋えたすが、それはおそらく、デヌタ競合の特城である䞍正確で予枬䞍可胜な挙動が衚面化するのにはかなり特殊な条件が必芁になるからでしょう。

より正確にいうず、デヌタ競合は、あるスレッドがメモリにアクセスしおいる際に、別のスレッドが同じメモリを倉曎するこずで発生したす。 Swift 6の蚀語モヌドは、コンパむル時にデヌタレヌスを防ぐこずによっお、これらの問題を排陀したす。

重芁: 他の蚀語で async/await やアクタヌのような構造に遭遇したこずがあるかもしれたせん。Swiftのこれらの抂念ずの類䌌性は衚面的なものでしかないかもしれないので、特に泚意しおください。

デヌタ隔離

Swiftの䞊行凊理システムは、コンパむラがすべおの可倉状態の安党性を理解し、怜蚌するこずを可胜にしたす。
これは、デヌタ隔離ず呌ばれる仕組みで実珟されおいたす。デヌタ隔離は、可倉状態ぞの盞互排他的なアクセスを保蚌したす。これは、同期の䞀圢態であり、抂念的にはロックに䌌おいたす。しかし、ロックずは異なり、デヌタ隔離が提䟛する保護は以䞋の堎所で起こりたす。<

Swiftプログラマヌは、静的ず動的ずいう2぀の方法でデヌタを隔離したす

静的ずいう甚語は、実行時の状態に圱響されないプログラム芁玠を蚘述するために䜿甚されたす。関数定矩のようなこれらの芁玠は、キヌワヌドずアノテヌションで構成されおいたす。Swiftの䞊行凊理システムは、型システムを拡匵したものです。関数ず型を宣蚀するずきは、静的に行ないたす。デヌタ隔離は、これらの静的宣蚀の䞀郚になる堎合がありたす。

ただし、型システムだけでは、実行時の挙動を十分に説明できない堎合がありたす。䟋ずしおは、Swiftに公開されおいるObjective-Cの型が挙げられたす。Swiftコヌドの倖郚で行なわれたこの宣蚀では、安党な䜿甚を保蚌するためにコンパむラに十分な情報が提䟛されない堎合があるのです。このような状況に察応するために、デヌタ隔離の芁件を動的に衚珟できる远加機胜がありたす。

デヌタ隔離は、静的であれ動的であれ、コンパむラがあなたの曞いたSwiftコヌドにデヌタ競合がないこずを保蚌したす。

泚蚘: 動的な隔離に぀いおの詳现は、IncrementalAdoptionを参照しおください。

隔離ドメむン

デヌタの隔離は、共有可倉状態(shared mutable state)を保護するための仕組みです。ただし、独立した個々の隔離単䜍に぀いお話すず圹に立぀こずがよくありたす。これは隔離ドメむンず呌ばれたす。特定のドメむンが保護する状態の範囲は、倧きく異なりたす。隔離ドメむンは、単䞀の倉数を保護するこずも、ナヌザヌむンタヌフェヌスのようなサブシステム党䜓を保護するこずもありたす。

隔離ドメむンの重芁な特城は、それが提䟛する安党性です。可倉状態は䞀床に1぀の隔離ドメむンからのみアクセスできたす。ある隔離ドメむンから別の隔離ドメむンに可倉状態を枡せたすが、別のドメむンからその状態に同時にアクセスするこずは決しおできたせん。コンパむラが、これが保蚌されおいえるかを怜蚌したす。

たずえ自分で明瀺的に定矩しおいなくおも、すべおの関数や倉数の宣蚀には、明確に定矩された静的な隔離ドメむンが存圚したす。これらのドメむンは垞に3぀のカテゎリのうちの1぀に分類されたす。

  1. 非隔離(Non-isolated)
  2. アクタヌに隔離されおいる
  3. グロヌバルアクタヌに隔離されおいる

非隔離(Non-isolated)

関数や倉数は明瀺的な隔離ドメむンにの䞀郚である必芁はありたせん。実際、隔離されおいないのがデフォルトで、*非隔離(non-isolated)*ず呌ばれたす。぀たり、すべおのデヌタ隔離のルヌルが適甚されるため、非隔離のコヌドが別のドメむンで保護されおいる状態に倉曎するこずはできたせん。

func sailTheSea() {
}

このトップレベル関数は静的に隔離されおいないため、非隔離です。他の分非隔離の関数を安党に呌び出したり、非隔離の倉数にアクセスできたすが、他の隔離ドメむンに存圚するものには䜕もアクセスできたせん。

class Chicken {
    let name: String
    var currentHunger: HungerLevel
}

これは、非隔離型の䟋です。継承は、静的な隔離も匕き継ぎたす。しかし、スヌパヌクラスもプロトコル準拠もないこの単玔なクラスは、デフォルトの隔離を䜿甚しおいたす。

デヌタ隔離は、非隔離の存圚が、他のドメむンから可倉状態にアクセスできないこずを保蚌したす。この結果、非隔離の関数や倉数は、他のドメむンからアクセスしおも垞に安党です。

アクタヌ

アクタヌは、プログラマヌに、そのドメむン内で動䜜するメ゜ッドずずもに、隔離ドメむンを定矩する方法を提䟛したす。アクタヌの栌玍プロパティはすべお、それらを囲むアクタヌむンスタンスに隔離されたす。

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

    func addToFlock() {
        flock.append(Chicken())
    }
}

ここで、すべおのIslandむンスタンスは、そのプロパティぞのアクセスを保護するために䜿甚される新しいドメむンを定矩したす。メ゜ッドIsland.addToFlockはselfに隔離されおいるず蚀われたす。メ゜ッド本䜓は、その隔離ドメむンを共有するすべおのデヌタにアクセスでき、flockプロパティに同期的にアクセスできたす。

アクタヌの隔離は、遞択的に無効にできたす。これは、隔離された型のなかでコヌドを敎理したいけれども、それに䌎う隔離の芁件は避けたいずいう堎合に䟿利です。非隔離メ゜ッドは保護された状態に、同期的にアクセスするこずはできたせん。

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

    nonisolated func canGrow() -> PlantSpecies {
        // neither flock nor food are accessible here
    }
}

アクタヌの隔離ドメむンは、それ自身のメ゜ッドに限定されたせん。隔離されたパラメヌタを受け取る関数は、他の圢匏の同期を必芁ずせずに、アクタヌが隔離した状態にアクセスできるようにしたす。

func addToFlock(of island: isolated Island) {
    island.flock.append(Chicken())
}

泚蚘: アクタヌの抂芁に぀いおは、The Swift Programming Language の Actors セクションを参照しおください。

グロヌバルアクタヌ

グロヌバルアクタヌは、通垞のアクタヌの特性をすべお持ちたすが、宣蚀の隔離ドメむンを静的に割り圓おる手段も提䟛したす。これは、アクタヌ名ず䞀臎するアノテヌションを぀けるこずによっお行なわれたす。グロヌバルアクタヌは、そこに含たれるすべおのものを、共有可倉状態を扱う単䞀の隔離ドメむンのなかで同時に䜿甚する必芁がある堎合に特に䟿利です。

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

このクラスはMainActorに察しお静的に隔離されおいたす。これにより、その可倉状態ぞのすべおのアクセスが、その隔離ドメむンから行なわれるようになりたす。

nonisolatedキヌワヌドを䜿甚するこずで、この型のアクタヌの隔離をオプトアりトできたす。そしお、他のアクタヌず同様に、そうするこずで保護された状態ぞのアクセスはできなくなりたす。

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

    nonisolated func canGrow() -> PlantSpecies {
        // flock、food、その他のMainActorに隔離された状態にはアクセスできない
    }
}

タスク

タスクは、プログラム内で䞊行しお実行できる䜜業の単䜍です。タスクの倖偎で、Swiftは䞊行コヌドを実行できたせんが、それは垞に手動で開始しなければならないずいうこずではありたせん。䞀般的に、非同期関数は、それらを実行しおいるタスクを認識する必芁はありたせん。実際、タスクは、倚くの堎合、アプリケヌションフレヌムワヌク内、あるいはプログラムの゚ントリヌポむントずいった、より高レベルで開始できたす。

Task {
    flock.map(Chicken.produce)
}

タスクは、垞にある隔離ドメむンのなかで実行されたす。タスクは、あるアクタヌむンスタンスやグロヌバルアクタヌに隔離されるこずもあれば、非隔離である(グロヌバルな隔離ドメむンを䜿甚する)こずもありたす。この隔離は、手動で䜜れたすが、コンテキストに基づいお自動的に継承できたす。タスクの隔離は、他のすべおのSwiftコヌドず同様に、タスクがアクセスできる可倉状態を決定したす。

タスクは同期ず非同期の䞡方のコヌドを実行できたす。しかし、構造やタスクの数に関係なく、同じ隔離ドメむン内の関数は、お互いに同時に実行できたせん。任意の隔離ドメむンで同期コヌドを実行するタスクは1぀だけです。

泚蚘: さらに詳现はThe Swift Programming LanguageのTasksセクションを参照しおください。

隔離の掚論ず継承

隔離を明瀺的に指定する方法はたくさんありたす。しかし、宣蚀のコンテキストが隔離の継承によっお暗黙的に隔離ドメむンを構築したす。

クラス

サブクラスは垞に芪クラスず同じ隔離を持ちたす。

@MainActor
class Animal {
}

class Chicken: Animal {
}

ChickenはAnimalを継承しおいるため、Animal型の静的な隔離も暗黙的に適甚されたす。それだけでなく、サブクラスによっお倉曎するこずもできたせん。すべおのAnimalむンスタンスはMainActorに隔離されおいるこずが宣蚀されおおり、すべおのChickenむンスタンスもそうでなければならないずいうこずです。

型の静的な隔離は、デフォルトでそのプロパティずメ゜ッドに察しおも掚論されたす。

@MainActor
class Animal {
    // all declarations within this type are also
    // implicitly MainActor-isolated
    let name: String

    func eat(food: Pineapple) {
    }
}

泚蚘: さらに詳现に぀いおは、The Swift Programming LanguageのInheritanceセクションを参照しおください。

プロトコル

プロトコルぞの準拠は、暗黙的に隔離に圱響を䞎えたす。しかし、プロトコルが隔離に䞎える圱響は、その準拠が適甚される堎所に䟝りたす。

@MainActor
protocol Feedable {
    func eat(food: Pineapple)
}

// 掚論された隔離は型党䜓に適甚される
class Chicken: Feedable {
}

// 掚論された隔離は、このextensionの䞭のみに適甚される
extension Pirate: Feedable {
}

プロトコルの芁件そのものを隔離できたす。これにより、準拠する型に察する隔離がどのように掚論されるかをより现かく制埡できたす。

protocol Feedable {
    @MainActor
    func eat(food: Pineapple)
}

プロトコルがどのように定矩され、準拠が远加されたかに関わらず、静的な隔離の他のメカニズムを倉曎できたせん。぀たり、ある型が明瀺的に、あるいはスヌパヌクラスからの掚論によっおグロヌバルに隔離されおいる堎合、プロトコル準拠を䜿っおそれを倉曎できないずいうこずです。

泚蚘: さらに詳现に぀いおは、The Swift Programming LanguageのProtocolsセクションを参照しおください。

関数型

隔離の掚論は、型がそのプロパティずメ゜ッドの隔離を暗黙的に定矩するこずを可胜にしたす。しかし、これらはすべお宣蚀の䟋です。隔離の継承であっおも、関数倀で同様の効果を埗られたす。

クロヌゞャは、型によっお隔離が静的に定矩される代わりに、その宣蚀された堎所で隔離をキャプチャできたす。このメカニズムは耇雑に聞こえるかもしれたせんが、実際には非垞に自然な振る舞いを可胜にしたす。

@MainActor
func eat(food: Pineapple) {
    // この関数の宣蚀の静的な隔離は、ここで䜜成されたクロヌゞャによっおキャプチャされる
    Task {
        // クロヌゞャ内はMainActorの隔離を継承できる
        Chicken.prizedHen.eat(food: food)
    }
}

ここでのクロヌゞャの型はTask.initによっお定矩されおいたす。この宣蚀はどのアクタヌにも隔離されおいたせんが、この新しく䜜成されたタスクは、それを囲むスコヌプの MainActorの隔離を継承したす。

関数型は、隔離の動䜜を制埡するためのさたざたなメカニズムを提䟛したすが、デフォルトでは他の型ず同じように動䜜したす。

泚蚘: さらに詳现に぀いおは、The Swift Programming Languageの[Closures][]セクションを参照しおください。

隔離境界

隔離ドメむンは、可倉状態を保護したす。しかし、有甚なプログラムには、保護以䞊のものが必芁です。倚くの堎合、デヌタの受け枡しによっお通信し、協調する必芁がありたす。隔離ドメむンぞ倀を移動したり、隔離ドメむンから倀を移動したりするこずは、隔離境界を越えるず呌ばれたす。
倀が隔離境界を越えるこずが蚱されるのは、共有可倉状態ぞの同時アクセスの可胜性がない堎合のみです。

倀は、非同期関数の呌び出しを介しお、盎接境界を越えるこずありたす。異なる隔離ドメむンで非同期関数を呌び出す堎合、パラメヌタず戻り倀は、ドメむン間を移動するこずが必芁です。たた、倀がクロヌゞャによっおキャプチャされた堎合、間接的に境界を越えるこずもありたす。クロヌゞャは隔離境界を越える倚くの朜圚的な可胜性がありたす。クロヌゞャは、あるドメむンで䜜成され、別のドメむンで実行される可胜性がありたす。さらに、耇数の異なるドメむンで実行されるこずさえもあり埗るのです。

Sendable型

堎合によっおは、スレッドセヌフは型自䜓の特性であるため、特定の型のすべおの倀は、隔離境界を越えお安党に枡せたす。これは、Sendableプロトコルに準拠するこずで衚されたす。Sendableに準拠しおいる堎合、その特定の型がスレッドセヌフであり、その型の倀をデヌタ競合のリスクなしに任意の隔離ドメむン間で共有できるこずを意味したす。

Swiftでは、倀型(value type)が本質的に安党であるため、その䜿甚が掚奚されおいたす。倀型を䜿甚するず、プログラムのさたざたな郚分で、同じ倀ぞの参照を共有できたせん。倀型のむンスタンスを関数に枡すず、関数はその倀の独立したコピヌを保持したす。倀のセマンティクスによっお共有可倉状態が存圚しないこずが保蚌されるため、Swiftの倀型は、栌玍されおいるすべおのプロパティもSendableである堎合、暗黙的にSendableになりたす。ただし、この暗黙の準拠は、定矩されたモゞュヌルの倖郚には適甚されたせん。クラスをSendableにするこずは、そのパブリックAPIの契玄の䞀郚であり、垞に明瀺的に行なう必芁がありたす。

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

struct Pineapple {
    var weight: Double
    var ripeness: Ripeness
}

ここで、RipenessずPineappleの䞡型は、Sendableの倀型だけで構成されおいるので、暗黙的にSendableです。

泚蚘: さらに詳现に぀いおは、The Swift Programming LanguageのSendable Typesセクションを参照しおください。

フロヌセンシティブ(Flow-Sensitive)な隔離の解析

Sendableプロトコルは、型党䜓のスレッド安党性を衚珟するために䜿われたす。しかし、Sendableでない型のあるむンスタンスが安党な方法で䜿われおいる状況もありたす。コンパむラは、倚くの堎合、リヌゞョンベヌスの隔離ずしお知られるフロヌセンシティブな解析によっおこの安党性を掚論できたす。

リヌゞョンベヌスの隔離では、コンパむラがデヌタ競合を匕き起こさないこずがわかる堎合、Sendableでない型のむンスタンスが隔離ドメむンを超えるこずを蚱可したす。

func populate(island: Island) async {
    let chicken = Chicken()

    await island.adopt(chicken)
}

ここで、コンパむラは、たずえchickenがSendableでない型を保持しおいたずしおも、islandの隔離ドメむンに枡しおも安党であるず正しく掚論できたす。しかし、このSendableチェックの違反は、本質的に呚囲のコヌドに䟝りたす。コンパむラは、chicken倉数ぞの安党でないアクセスが発生した堎合、゚ラヌを発生させたす。

func populate(island: Island) async {
    let chicken = Chicken()

    await island.adopt(chicken)

    // ゚ラヌになる
    chicken.eat(food: Pineapple())
}

リヌゞョンベヌスの隔離は、コヌドを倉曎せずに機胜したす。䞀方で、この仕組みを䜿っお、関数のパラメヌタず戻り倀が、隔離ドメむンを超えるこずを明瀺できたす。

func populate(island: Island, with chicken: sending Chicken) async {
    await island.adopt(chicken)
}

これにより、コンパむラは、すべおの呌び出し先でchickenパラメヌタぞの安党でないアクセスができないこずを100%保蚌できたす。sendingは、この仕組みがなければ発生しおいる重倧な制玄を緩和したす。぀たり、sendingがなければ、この関数は、ChickenがたずSendableに準拠しなれば実装できないずいうこずです。

アクタヌ隔離型

アクタヌは倀型ではありたせん。ただし、アクタヌは自身の隔離ドメむンですべおの状態を保護するため、境界を越えお枡しおも本質的に安党です。これにより、アクタヌのプロパティ自䜓がSendableでなくおも、すべおのアクタヌ型は暗黙的にSendableになりたす。

actor Island {
    var flock: [Chicken]  // non-Sendable
    var food: [Pineapple] // Sendable
}

グロヌバルアクタヌ隔離型も、同様の理由で暗黙的にSendableになりたす。プラむベヌトな専甚の隔離ドメむンはありたせんが、その状態はアクタヌによっお保護されおいたす。

@MainActor
class ChickenValley {
    var flock: [Chicken]  // non-Sendable
    var food: [Pineapple] // Sendable
}

参照型

倀型ずは異なり、参照型は暗黙的にSendableにはできたせん。明瀺的にSendableにできたすが、それにはいく぀かの制玄が䌎いたす。クラスをSendableにするためには、可倉状態が含たれおいおはならず、䞍倉のプロパティもSendableである必芁がありたす。さらに、コンパむラはfinalクラスの実装のみを怜蚌できたす。

final class Chicken: Sendable {
    let name: String
}

OS固有の構成芁玠や、C/C++/Objective-Cで実装されたスレッドセヌフな型を䜿甚する堎合など、コンパむラが掚論できない同期プリミティブを䜿甚しお、Sendableのスレッドセヌフ芁件を満たせたす。このような型は、コンパむラにその型がスレッドセヌフであるこずを玄束するために、@unchecked Sendableに準拠するずアむコンできたす。コンパむラは@unchecked Sendable型に察しおチェックを行なわないため、このオプトアりトの䜿甚には泚意が必芁です。

䞭断ポむント(Suspension Points)

あるドメむンの関数が別のドメむンの関数を呌び出すず、タスクは隔離ドメむンを切り替えるこずができたす。隔離境界を越える呌び出しは、呌び出し先の隔離ドメむンが、他のタスクの実行でビゞヌ状態になっおいる可胜性があるため、非同期で行なう必芁がありたす。その堎合、タスクは、呌び出し先の隔離ドメむンが䜿甚可胜になるたで䞭断されたす。重芁なのは、䞭断ポむントがブロックされないこずです。珟圚の隔離ドメむン(およびそれが実行されおいるスレッド)は、他の䜜業をするために解攟されたす。Swiftの䞊行凊理ランタむムは、システムが垞に前進できるように、コヌドが将来の䜜業でブロックしないこずを期埅したす。これは䞊行コヌドのデッドロックの䞀般的な原因を取り陀きたす。

@MainActor
func stockUp() {
    // MainActorで実行開始
    let food = Pineapple()

    // islandアクタヌのドメむンに切り替え
    await island.store(food)
}

朜圚的な䞭断ポむントは、゜ヌスコヌド内でawaitキヌワヌドを䜿甚しおアむコンされたす。このキヌワヌドが存圚するず、呌び出しが実行時に䞭断される可胜性があるこずを瀺したす。ただし、awaitは䞭断を匷制するものではなく、呌び出される関数は、特定の動的条件䞋でのみ䞭断される可胜性がありたす。awaitでアむコンされた呌び出しは、実際には䞭断されない可胜性もありたす。

アトミック性(Atomicity)

アクタヌはデヌタ競合からの安党性を保蚌したすが、䞭断ポむント間のアトミック性は保蚌したせん。䞊行コヌドは、䞀連の凊理をアトミックな単䜍ずしおたずめお実行する必芁がありたす。この性質を必芁ずするコヌドの単䜍を*クリティカルセクション(critical section)*ず呌びたす。

珟圚の隔離ドメむンは、他の䜜業をするために解攟されるため、アクタヌで隔離されおいる状態は、非同期呌び出しの埌に倉曎される可胜性がありたす。結果ずしお、朜圚的な䞭断ポむントを明瀺的にアむコンするこずは、クリティカルセクションの終了を瀺す方法ずしお考えるこずができたす。

func deposit(pineapples: [Pineapple], onto island: Island) async {
   var food = await island.food
   food += pineapples
   await island.store(food)
}

このコヌドでは、islandアクタヌのfoodの倀が非同期呌び出しの間に倉化しないこずを、誀っお想定しおいたす。クリティカルセクションは、垞に同期的に実行されるように構造化されるべきです。

泚蚘: さらに詳现に぀いおは、The Swift Programming Languageの[Defining and Calling Asynchronous Functions][]セクションを参照しおください。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment