SE0337-Incremental migration to concurrency checking
Introduction
Swift5.5では、言語の機能として、データ競合を防ぐ機能が追加された、それは、SE-0302のSendableや、SE0316のGlobalActorを含む。(Sendable: どの型がacross Task/ across actorで安全に扱えるかをチェックできる。GlobalAcotr: mainActorなどでの適切な動機的処理を保証する)
しかし、Swift5.5の段階では、SendableやMainActorの使い方についての制約を強制してはいなかった。
それは、SwiftCuncurrency対応をしてないモデュールとやりとりするのがとても面倒になるからである。
Develperに役に立つであろう、Concurrencyの機能の導入とConcurrency対応してないModuleとうまくやっていく方法を提案する。
これによって、スマートにSwiftからデータ競合を排除する。
Motivation
SwiftConcurrencyの目的は、データ競合を、並行プログラムにおけるisolating stateの機能を導入することで、排除することである。
主要な機能としては、Sendable Checkingがある。
データをタスクやActroの境界を跨いでやりとり数場合は、inputがSendable Protocolに準拠している必要があ理、型は、安全に送れるように宣言する必要があり、コンパイラーは、それらの型が全てSendableになっているかをチェックする。
この機能はSwift6あたりで導入されるため、既存の数百万のライブラリやC.Obj-Cと相互運用していく必要がある。
それらのコードにはSendable checkができるように実装されてはいないが、それらがConcurrency対応するまでの間でも、私たちはSwiftからそれらの機能を使いたい。(Sendable対応前と後のコードが、共存できる仕組みが必要)
対応が難しいと考える領域がいくつかある。
ライブラリにConcurrency annotationを追加する。
多くの既存のAPIは並行プログラミングへの対応を公式的にする必要がある(明示的にSendable などの宣言をして、コンパイラにチェックしてもらえるようにする)
例えば、UIKitのメソッドやプロパティの多くは、MainThreadからのみ呼び出されることになっている。
しかし、@MainActorが登場する前だったので、これらの制約は、ドキュメントやassertに書くくらいのことしたできず、コンパイラに知らせる方法がなかった。
それゆえ、多くのモジュールで、APIのどの部分にannotationをつけるべきか包括的な調査を行う必要がある。しかし、たとてそれがツールなどでできたとしても、既存のソースコードへの破壊的変更になってしまう。
例えば、もし methodに"MainActor"とつけたなら、Concurrency対応してないコードから呼び出す時は、例え正しい使用法をしていたとしても、うまく呼び出せなくなる。
なぜなら、そのプロジェクトはコンパイラに対して、使用法が正しいことを証明してないからである。(MainActorをつけて、証明する必要がある。出なければコンパイルエラーになる)
幾つかのケースで、ABIの破壊的変更を伴う。(Application Binary Interface).
例えば、関数型についたSendableや、Generic ParamのSendable制約は、Sendableへの準拠が呼び出す慣習に影響がないにも関わらず、関数名にmangleされる。(Magle: 名前修飾、多分関数名に @Sendableの部分も含まれるということ?)
(例えば、 witness tableの追加の引数もない)
この機能は、型チェックの時の制約として必要だが、生成されるコードでは存在しないかのように扱われる。(コンパイル時にチェックする用途のみで使われる)
ここでは、私たちは以下の機能が必要である
- Concurrency対応してないモジュールのための、 "compatibility mode"
- concurrencyの対応でsignatureが変わってしまったAPIについて、宣言をマークする方法。
使ってるmoduleが更新される前に、SendableCheckを適応する。
LibaryにConcurrency annotationsをつけていく作業は時間がかかる。
自分が使ってるすべてのLIbraryがSendable対応してから、自分のModuleのSendable対応を始めるというのは現実的ではない。
これは、importしているLibraryが完全にConcurrency対応してなくても、Module側はそれとうまくやっていくための回避策が必要がある。
(importの宣言の時に調整するか、コンパイラにエラーを無視してもらうかなどが考えられる)
どんな機能を使っていても、そのような冗長にはしたくない)
例えば、全てのNon-Sendableな変数に"Sendable"つけていくのはとても面倒である
また、ライブラリが完全にConcurrency対応を完了して、クライアントがConcurrencyについて大きな勘違いをしていた時、に何が起こるかを注意する必要がある。
例えば、Geometry ModuleからType型をimportしたとする。あなたは、Geometry ModuleがConcurrency対応を完了させる前に、Sendable Checkを有効にした。そのため、違うActorにPoint型の変数を送るときにSendable Checkが行われる。
Point型の既知の情報からあなたはSendableになるだろうと仮定し、Sendable Checkを黙らせることにした。
しかし、Geometryのメインテナーは、後からPointの実装を確かめて、Concurrency Domainを跨いで送信することは危険であると判断した。
そのため、GeometryModuleでは、pointはNon-Sendableになった。
その後に、あなたがプロジェクトのGeometryModuelをUPdateして、ビルドしようとしたときに、何が起こるだろうか?
理想的には、Swiftはこのバグに関する診断を抑制し続けるべきではない。(ConcurrencyCheckを有効、強制にすべきという意味?)
結局GeometryteamがPointをNon_Sendableとして、それはあなたのSendableだろうという予想よりも決定的である。
一方、もしかしたらそれはあなたもプロジェクトのrebuildを邪魔するべきでないのかもしれない、なぜならそのバグは回帰バグではないから。
GeometryModuleの更新はあなたのプロジェクトにバグを発生させなかったが、あなたのコードはすでに、バグが存在する。
それはただコードにバグがあったことが明らかになっただけです。
診断でバグが見つからないよりも、見つかった方が、改善に向かっているということは間違いない。
しかし、もし、機能はビルドできていたものが Swiftがバグを発見することで、ビルドできなくなったら、あなたは、GeometryModuelの更新を遅らせたり、 GeometryModuleのメインテナーに直すまで更新を遅らせるようにプレッシャーをかけるかもしれない。
あなたがModuleの仕様について後で間違った使い方をしていたと分かったとしても、SwiftはそのバグについてエラーではなくWarningを出すべきである。それによって、バグを認識できるが、ビルドをするためにすぐに直す必要はない。
ここで必要となってくるのは
- 特定の宣言やモジュールに関して Concurrency Annotationがついてないことに対する診断を黙らせる方法
- Concurrency Annotationが追加された時に、間違った使い方をしていたら、エラーではなくwarningを出す規則
Proposed solution
Concurrency Annotation対応を助ける幾つかの機能を提案する、特にSendable Check
これらの機能は、以下のWorkFlowでConcurrencyCheck対応をできるように設計されている。
- Swift6モードを有効にして concurrecnyの機能を使って(async/await, actor)、concurrency checkを有効にする. (-warn-concurrency flagをつけることで有効にできる)これによって、concurrency constraintを違反している箇所にはエラーかwarningが出る。
- それらの問題に対処を開始する。それは他のモジュールの型に関係する問題なら、Xcodeに提案される"@preconcurrency" importをすることでそれらの warningを消すことがきる。
- そべての問題を解決したら、より大きなビルドに変更を統合する。(小さいModuleから始めて、最後に合体させよう的なこと?)
- 将来のある時点で、importしているModuleがConcurrency対応を完了させるかもしれない。それに更新して、コード上に違反が新しく発生したら、それらについてはwarningが表示されるだろう。それらは新しく発生したバグなので、直そう
- 全ての問題を解決したら(全ての問題が解決されたら、preconcurrencyを取り除くように進めるwarningがでてくるだろう)そうしたら、 @preconcurrecyを取り除こう。それ以降そのモジュールに関係する全てのSendablecCheckの問題は、エラーとして表示され、それを直すまでビルドができなくなる。
これらを達成するために幾つかの機能連携して動作する必要がある。
- Swift6モードでは、全てのコードがSendableConformatnceや他のConcurrency制約への違反を完全にチェックされる。 -warn-concurrency flagは古い言語のバージョンで、それらの診断をwarningとして表示してくれる。
- Nonimal Declarationにpreconcurrency Attributeを適応するとき、それは宣言がそれをconcurrencyCheckのために更新するために変更したということを意味する。そのため、compilerはSwift5でのCOncurrencyCheckに違反する幾つかの使用方法を許可し、pre-concurrency binaryを相互運用できるコードを生成する。
- import文に@preconcurrency属性を適用すると、その型が明示的に,利用できないもしくは制約を満足してないSendable適合性を宣言している場合にのみ、そのモジュールからSendableでない型のSendableを要求する使用を診断するようにコンパイラに指示します。その場合でも、エラーではなくワーニングになる。??
Detailed design
Recovery behavior
このプロポーザルの、エラーがwarningとして出力される場合や、抑制されることについて言及している時、それは、compilerが以下のように振る舞って 回復するということを意味している。(優先順位に従って)
- Sendableに準拠してないNonimal typeは、Sendableになる。
- SendableやGlobalACtorのついたfunction型には、それがない?
Concurrency checking modes
Swift の全てのscopeは、次の二つの concurrency checking modeのうち一つを持つとして説明できます。
- "Strict concurrency checking"
- Sendableの準拠し忘れや、GlobalActorのつけ忘れは、チェックされる
- Swift6では、エラーとなる。 Swift5モードで "@preconcurrency import"がついてるnominal declarationは、warningとなる。
- "Minimal concurrency checking"
- Sendableの準拠し忘れや、GlobalActorのつけ忘れは、warningになる "@preconcurrency"は多くの診断結果を抑制する特別な効果を持つ。
最上位スコープの ConcurrencCheckModeは以下の通りです。
- "Strict"
- モジュールがSwift6以降のモードでコンパイルされる時
- Swift6以前のモードで "-warn-concurrency" flagを使ってコンパイルされる時
- パースされるファイルがmodule interfaceの時
- "Minimal"
- それ以外の時
Child scopeのConcurrency checkingモードは以下の通り
- Strict: 親のconcurrency checking modeがmininalで、下のうち どれかを満たす
- GlobalActorが明示された closureである。
- async かSendableのclosureかautoclosure
- 明示的にnonisolatedかGlobalActorとして宣言されている
- asyncやsendableとしてマークされたfunction, method, initializer, accessor, variable, subscript
- actorの宣言
- Minimal: その他の場合は、parent scopeと同じになる。
"Imported C declaration" は Minimal concurrency checkのscopeになる。
preconcurrency attribute on nominal declarations
Concurrency Behaviorを説明するために、maintainerは 幾つかの既存の宣言を変える必要がある。
それは、pre-concurrency コードに破壊的変更や、過去にコンパイルされたbinaryのABIを破壊するかもしれない。
特に、下のことをする必要がある。
- 関数型にSendableやGlobalActorをつける
- Genericを使っているところにSendable制約をつける
- 宣言にGlobalActorをつける。
"Nonimal declaration”をConcurrency対応する際は、@preconcurrencyは、Moduleが完全にConcurrency対応を終える以前に、宣言が存在していたことを示す。
そのため、compileerはソースコードやABIの破壊的変更を防ぐために段階を踏む必要がある。
それは、任意の enum, enum case, struct, class, actor, protocol, var, let, subscript, initや関数の宣言に適応される。
もし、 nominal declarationが@preconcurrencyを使用していたら。
- その名前が 列挙した機能を全く使用してないかのように変形される。
- mininal concurrency checkを使用している箇所を内包している使用箇所は、compilerはtraitsのmismatchに関する診断結果を抑制する
- ABI checkerは バイナリを生成するときに、それらの機能の使用をとりのぞく
Objective-Cの宣言は、"preconcurrency"がつけられているかのように、常にimportされる。
例えば、MainActorから下呼ばれない関数を考える。
そこでは、違うタスク(Task context)で 与えられたclosureを実行する。
@MainActor func doSomethingThenFollowUp(_ body: @Sendable () -> Void) {
// do something
Task.detached {
// do something else
body()
}
}
この関数は、MainActorやSendableのアノテーションがないなら、concurrency対応以前から存在していた可能性がある。
Concurrencyアノテーションをつけた後は、以前までは動いていたコードがエラーになる。
class MyButton {
var clickedCount = 0
func onClicked() { // always called on the main thread by the system
doSomethingThenFollowUp { // ERROR: cannot call @MainActor function outside the main actor
clickedCount += 1 // ERROR: captured 'self' with non-Sendable type `MyButton` in @Sendable closure
}
}
}
しかし、"doSomethingThenFollowUp"に"preconcurrency"をつけると、その型は"MainActor"と"Sendable"を消して調整され、エラーがなくなり、これまでの(Concurrency対応以前の)同じ type interfaceになる。
"doSomethingThenFollowUp"におけるこの違いは minialとstrictモードの違いによって見えるようになる。
func minimal() {
let fn = doSomethingThenFollowUp // type is (( )-> Void) -> Void
}
func strict() async {
let fn = doSomethingThenFollowUp // type is @MainActor (@Sendable ( )-> Void) -> Void
}
Sendable conformance status
型は、下の3つのSendableの準拠状態によって表現することができる。
- Explicit Sendable
- 実際にSendableに準拠している(明示的に宣言している、もしくはSE0303で述べらているルールによってSendableだと予想された型)
- Explicitly non-Sendable
- Sendableへの準拠が型の宣言に明記されているが、その型が利用できないか、満たしてない制約があるか、Strict concurrency checkingのscopeで宣言されている場合(注2)
- Implicitly non-Sendable
- Sendableの準拠が宣言されていない場合
注2
これは、moduleがSwift6か"warn-concurrency" flag付きでcompileされていて、それらの全ての型が 明示的にSendableか、明示的にnon-Sendable である場合。
型は 利用できないSendable 準拠をすることで、明示的にnon-sendableになる。
@available(*, unavailable)
extension Point: Sendable { }
このような準拠はSendableへの明示的でない準拠を抑制する。
@preconcurrency on Sendable protocols
既存のprotocolのいくつかは、Sendabgleとして表現されるべきである。
そのようなProtocolがConcurrency対応する時に、SendableProtocolを継承するとだろう。
しかし、そのようにすると、Sendableになったprotocolを準拠している型はSendableであることが求められるようになるため、破壊的変更を生じる。
この問題はSE0302で説明されている。なぜなら、これは標準ライブラリのErrorとCodingKey Protocolに影響があるからである
protocol Error: /* newly added */ Sendable { ... }
class MutableStorage {
var counter: Int
}
struct ProblematicError: Error {
var storage: MutableStorage // error: Sendable struct ProblematicError has non-Sendable stored property of type MutableStorage
}
この問題に対処するために、SE-0302では Errorプロトコルの Sendable追加について以下のように述べている。
簡単な transitionのために、Error型によってSendableが必要になってエラーになっている型は、Swift6以前ではエラーでなくwarningを出すようにする。
ここでは、preconcurrencyとついたものやSendableを継承している全てのprotocolに対して適応するために、ErrorとCodingKeyのためのルールを置き換える提案をする。
標準ライブラリの二つのprotocolが@preconcurrencyを使用する予定である。
@preconcurrency protocol Error: Sendable { ... }
@preconcurrency protocol CodingKey: Sendable { ... }
@preconcurrency attribute on import declarations
"@preconcurrency"はimport宣言につけることができる。
そうすると、そのモジュールからimportされた方によって生じるconcurrency checkのいつくかの違反に対して、コンパイラーによるチェックの厳しさを弱くする。
Concurrency対応をまだ完了していないmoduleに対して、使うことができる。
そうすると、コンパイラはSendableになるべき全ての型がannotated されたら あなたに教えてくれる。
また、あなたの間違った家庭をモジュールが修正してくれるまでの間、あなたのプロジェクトがコンパイルできる状態でいるための、一時避難所として機能してくれる。
@preconcurrencyとついているimportには以下の効果が適応される。
- Sendableが必要なところで、非明示的にnon-sendableになっている箇所
- その型がpreconcurrency importによって見えるなら、診断が抑制される(Swift6以前) or warningを表示する(Swift6以降)
- そうでないなら、通常通りの診断結果が表示される、しかし、それとは別に、その問題を回避するためにpreconcurrency importを使うようにお薦めする診断が表示される
- Sendableが必要なところで、明示的にnon-sendableになっている場所
- @preconcurrency importによって見えているなら、エラーでなくwarningが表示される。これはSwift6以降でも。
+ そうでないなら、通常の診断が表示される。
- @preconcurrency importによって見えているなら、エラーでなくwarningが表示される。これはSwift6以降でも。
- preconcurrencyが役に立ってない時は(注3)、それを取り除くように勧めるwarningが表示される
注3
十分に明確に定義できるかわからないため、unusedに対する明確な定義をしない。
例えば、preconcurrency import間で依存関係がある場合などが難しいから。?
Source compatibility
このプロポーザルは、コードの互換性の問題に動機付けられている。
preconcurrencyの正しい仕様は、Minimal Concurrency Checkによるビルドでの破壊的変更を防ぐ。
projectが、依存先のmoduleたちがconcurrenct対応を終える前に、strict concurrency対応を終えてしまっている場合に、preconcurrency importは一時的にconcurrency checkのルールを弱くする。
Effect on ABI stability
preconcurrencyだけでは、ABIに影響がない。
もし、すでに影響がある機能を適応してある宣言に対して適応したときは、ABIの互換性の問題を生じる。
しかし、そのような機能は同時もしくは後から追加されるため、ABIへの破壊的変更は起こらないだろう。
Sendable concurency errorを無効にするpreconcurrencyの機能は、現行のABIと互換性がある。
なぜなら、Sendableは追加のmeta-dataを排出せず、渡される必要のあるwitness tableを持っている用に設計されている。
もしそうではないなら、呼び出しの慣習や他の ABIに影響があっただろう。
これは、name manglingのみに影響がある。
よってABIへの影響はない。