SE-0414: Region based isolation

Introduction
Swift Concurrencyは、変数を actorとTaskの境界によって定められるisolation domainに割り当てる。
異なるisoaltoin domainで実行されているコードは、並行実行することができる。そして、Sendable Checkingは non-Sendableな変数がisolation boundariesを跨いで渡されることを完全に防ぐ。
実例として、これはとても厳しい、semanticな制約である。
なぜなら、それはdata raceが発生しない自然なプログラムの書き方を禁止するからである。(Data raceには関係のない書き方も制限される)
この文書では、新しい制御フローを導入することで、non-Sendableな変数がisolatoin boundaryを跨いで安全に受けわたしができるかを判断している、それらの制約を軽減することを提案する。
これは、isolation regionという考えかたを導入することで、達成できる。
それによって、コンパイラは二つの変数がお互いに影響しあう場合に保守的に推論できるようになる。
Isolation regionの使用を通して、言語は、non-Sendableな変数をisolation boundaryを跨いで送ることは、その変数が受け渡しの後にcallerから使用されないことを保証することで、data raceを引き起こさないことを証明できる。
(isolation domainを跨ぐときに、所有権も渡すかんじ?)

Motivation
SE-0302では、Non-Sendableな変数はisolatio boundariesを越えて渡すことはできないと定めている。
下記のコードは、actor-isolated functionに新しく作成した値を渡すときに、Sendable Checkに違反する例である。
// Not Sendable
class Client {
init(name: String, initialBalance: Double) { ... }
}
actor ClientStore {
var clients: [Client] = []
static let shared = ClientStore()
func addClient(_ c: Client) {
clients.append(c)
}
}
func openNewAccount(name: String, initialBalance: Double) async {
let client = Client(name: name, initialBalance: initialBalance)
await ClientStore.shared.addClient(client) // Error! 'Client' is non-`Sendable`!
}
これは、過剰に保守的である。このコードは以下の理由で安全である。
-
client
は、propertyがStringとDoubleであり、コンストラクタからNon-Sendableな値へのアクセスをしていない。 -
client
はopenNewAccount
のそとで使うことができない。 -
client
はaddClient
を越えて使用されていない。
-
client
is not used withinopenNewAccount
beyondaddClient
.
この単純な例は、Strict Concurrency checkの過剰な制限を示している。
開発者は、data raceが起こる心配のない所でも、安全な避難場所が必要になる。(e.g @unchecked sendable)

Proposed solution
この提案では、新しい制御フローを導入する。
そのフローは、non-Sendableの値をisolation bounradiesを跨いで渡すことを可能にするが、既に他のisolation domainに渡されているnon-Sendableな値を使用する箇所ではエラーを表示する。
これは、上記の例をvalidなコードにする。
client
変数は、ClientStore.shared
actorに渡されたあとに、使用されてないからである。
もし、openNeAccount
を改変して、addClientのあとに、client
のメソッドを呼び出そうとすると、それは違反になる。
なぜなら non-sendableな変数は non-isolated contextから、actor isolated contextに渡されているから、平行アクセスされる可能性があるからである。
func openNewAccount(name: String, initialBalance: Double) async {
let client = Client(name: name, initialBalance: initialBalance)
await ClientStore.shared.addClient(client)
client.logToAuditStream() // Error! Already transferred into clientStore's isolation domain... this could race!
}
addClient
の呼び出しの後は client
から参照されないことが静的に証明されている、他のnon-Sendableな変数は 引き続き安全に使用することができる。
この証明は、isolation regionsの考え方を使う
isolation regionは、変数の集合であり、その中の変数は、同じ集合の変数からのみ参照される。(参照を辺としたグラフの連結成分)
正式には、二つの変数 x,y は、program point pにおいて、 以下の条件を満すときに同じ isolation regionとする。
- pにおいて、xはyの別名である。
- xかxのプロパティは、yから参照可能である。(via y propertyからのchained accessで)
これは、違うisolatoin regionのnon-Sendableな変数は、平行に扱うことができることを保証する。
なぜなら、xを使用するコードは、に影響を与えることができないからである。
let john = Client(name: "John", initialBalance: 0)
let joanna = Client(name: "Joanna", initialBalance: 0)
await ClientStore.shared.addClient(john)
await ClientStore.shared.addClient(joanna) // (1)

let john = Client(name: "John", initialBalance: 0)
let joanna = Client(name: "Joanna", initialBalance: 0)
await ClientStore.shared.addClient(john)
await ClientStore.shared.addClient(joanna) // (1)
上記の例では二つのClient
インスタンスを作成している。
johnが、joannaを参照することは不可能である、(逆も)
なので、この二つの変数は異なるisolation regionに属する。
異なるisolation regionに属する変数は、平行に扱うことができる、なので、joann at (1)はClientSTore,sharedのなかで johnにアクセスするコードと平行に実行されるかもしれないが、data raceは発生しない。
対照的に、Client
にfriend
propertyを追加して、john,friend = joann
としたなら、
let john = Client(name: "John", initialBalance: 0)
let joanna = Client(name: "Joanna", initialBalance: 0)
john.friend = joanna // (1)
await ClientStore.shared.addClient(john)
await ClientStore.shared.addClient(joanna) // (2)
joannaはjohn.friendを経由してアクセスされるため、二つは同じisolation regionに属することになる。
ので、data raceに注意する必要がある。

これまでダメだったコードがOKになるだけなので、互換性へん影響がない