概要
actor のメソッドやプロパティは actor に isolate されています。 SE-0313 Improved control over actor isolation のプロポーザルで追加された isolated
/ nonisolated
キーワードを使うことで、 actor のメンバーでない処理を actor に isolate したり、逆に actor のメンバーを actor に isolate しないことができます。この記事では、プロポーザルをもとにこれらのキーワードの使い方をまとめます。
記事中の動作検証は Xcode 14 Beta 5 で行っています。
isolated
キーワード
actor の例として、以下の Counter
考えます。
actor Counter {
var count: Int = 0
}
この actor の count
を外部からインクリメントしたいとします。 actor に isolate された状態はデータ競合から保護するために外部から直接操作できないようになっているため、以下のような実装はエラーになります。
func incrementCounter(_ counter: Counter) {
counter.count += 1 // ❗️ Actor-isolated property 'count' can not be mutated from a non-isolated context
}
ここで、パラメータに対して isolated
キーワードをつけることで、 incrementCounter
関数を counter
actor に isolate することができます。これにより、同期的に actor のプロパティである count
を操作できるようになります。
func incrementCounter(_ counter: isolated Counter) {
counter.count += 1 // ✅ OK
}
この incrementCounter
は async
関数ではありませんが、 actor に isolate されているために actor 外から呼ぶ際には await
する必要があります。actor に isolate されている以上 actor のメソッドと同じ性質を持つというわけです。
@main
public struct Main {
public static func main() async throws {
let counter = Counter()
await incrementCounter(counter)
}
}
isolated
関連のエラー
isolated
キーワードは actor にしかつけられないようになっています。関数が isolated
がつけられたパラメータに isolate されるという性質上、 actor でないパラメータに isolated
をつけるのは意味をなさないためです。
// ❗️ 'isolated' parameter has non-actor type 'Int'
func incrementCounter(_ counter: Counter, by value: isolated Int) {}
また、 isolated
を1つの関数の中で複数のパラメータにつけることはできません。これは関数を複数の actor に isolate することができないためです。
// ❗️エラー
func incrementCounters(_ counter1: isolated Counter, _ counter2: isolated Counter) {}
同様の理由で、 actor のメソッドに isolated
キーワードをつけることもできません。
extension Counter {
// ❗️エラー
func transferCount(to other: isolated Counter) {}
}
ただし、 Xcode 14 Beta 5 の時点ではいずれのコードもコンパイルエラーや警告なしで実行することができ、試してみると実際にデータ競合を発生させることもできてしまいます。この問題はそのうち修正され、上記のコードは実行できなくなるはずです。
ref: https://github.com/apple/swift/issues/60474
isolated
の使いどころ
isolated
キーワードですが、絶対に使わなければいけない場面というのは思いつきません(もしご存知の方がいればコメントで教えてください)。 actor に isolate された処理がほしければ actor のメソッドにしてしまえばいいためです。
ただし、コード管理やコードの書き味の観点で actor のメソッドよりもグローバルな関数にして isolated
キーワードを使う方が適切な場面というのはありそうなので、覚えておいて損はないでしょう。
nonisolated
キーワード
isolated
キーワードを使うことで actor 外の関数を actor に isolate することができることがわかりました。 nonisolated
キーワードを使うことで、逆に actor のメンバーを isolate しないことができます。
例えば、以下のように actor のメソッドに nonisolated
をつけると、 isolate されなくなるため actor 外から同期的に呼べるようになります。
extension Counter {
func f1() {}
nonisolated func f2() {}
}
@main
public struct Main {
public static func main() async throws {
let counter = Counter()
counter.f1() // ❗️ Expression is 'async' but is not marked with 'await'
counter.f2() // ✅ OK
}
}
nonisolated
キーワードは var
プロパティにつけるとエラーになります。そもそも actor は可変な状態を守るためのものなので、 var
を isolate しないのは actor の使い方から外れるためにこのようになっているのではないかと思います。
actor Counter {
nonisolated let id: String
nonisolated var count: Int = 0 // ❗️ 'nonisolated' can not be applied to stored properties
nonisolated var p1: String { "p1" }
nonisolated func f1() {}
}
また、 let
プロパティは nonisolated
にすることは可能ですが、おそらく実際にするメリットはあまりないのではないかと思っています。let
にはデフォルトで actor 外から同期的にアクセスできるようになっているためです。これは、データ競合が起こるには同じデータに同時にアクセスが発生しそのうち1つが書き込みである必要がありますが、 let
には書き込みができないためデータ競合が起きる余地がないのでそうなっているのだと思います。
actor Counter {
let id: String
init(id: String) {
self.id = id
}
}
@main
public struct Main {
public static func main() async throws {
let counter = Counter(id: "counter")
print(counter.id) // ✅ OK。 nonisolated がついていなくても同期的にアクセスできる
}
}
nonisolated
の使いどころ
nonisolated
が役立つ場面として、例えば複数のスレッドから同時にアクセスしても安全な actor の computed プロパティに対して同期的にアクセスできるようにすることが挙げられます。
Counter
の概要を文字列で返す以下の description
プロパティを考えます。
actor Counter {
let id: String
var count: Int = 0
var description: String {
"Counter with ID \(id)"
}
init(id: String) {
self.id = id
}
}
description
プロパティは actor に isolate されているので actor 外からは await
をつけて呼ぶ必要があります。同期関数からそのまま呼べないため不便です。
@main
public struct Main {
public static func main() async throws {
let counter = Counter(id: "counter")
print(counter.description) // ❗️ Expression is 'async' but is not marked with 'await'
}
}
しかし、 description
の実装を冷静に見直してみると let
プロパティである id
にしか依存していません。先ほども説明したように let
プロパティではデータ競合が発生せず複数スレッドから同時にアクセスしても安全なため、 description
も同様に複数スレッドから同時にアクセスできることになり、 actor の保護が必要ないということになります。つまり、 description
は actor に isolate する必要がないのにも関わらず isolate されているせいで非同期にしかアクセスできず不便になっていることになります。このような状況で description
に nonisolated
をつけて actor に isolate されなくすることで、同期的にアクセスすることが可能になります。
actor Counter {
let id: String
var count: Int = 0
nonisolated var description: String {
"Counter with ID \(id)"
}
}
@main
public struct Main {
public static func main() async throws {
let counter = Counter(id: "counter")
print(counter.description) // ✅ OK
}
}
ここで、もちろん actor に isolate するべきプロパティに対して nonisolated
な処理からアクセスできてしまうとデータ競合の原因になります。そのような危険なアクセスに対してはちゃんとコンパイラがエラーを出してくれるため、我々は安心して nonisolated
を使うことができます。
actor Counter {
let id: String
var count: Int = 0
nonisolated var description: String {
// ❗️ Actor-isolated property 'count' can not be referenced from a non-isolated context
"Counter with ID \(id) with count \(count)"
}
}
nonisolated
が役立つ別の場面として、 actor を特定の protocol に準拠させたい時があります。例えば、 Counter
を CustomStringConvertible
に準拠させたいとします。 CustomStringConvertible
の要求は
public protocol CustomStringConvertible {
var description: String { get }
}
のみですが、以下のような Counter
の実装だと準拠がうまくいかずエラーになってしまいます。
actor Counter: CustomStringConvertible {
let id: String
// ❗️ Actor-isolated property 'description' cannot be used to satisfy nonisolated protocol requirement
var description: String {
"Counter with ID \(id)"
}
}
これは、 description
が isolate されていることから actor 外からは非同期に呼ぶ必要があるため、インターフェースとしては async
なプロパティになっているためです。 CustomStringConvertible
の要求として description
に同期的にアクセスできないといけないことになっているため、非同期にしかアクセスできないようではこの要求を満たすことができません。
このような場面で nonisolated
が有効です。 description
に nonisolated
をつけることで actor 外からも同期的にアクセスできるようになるため、 CustomStringConvertible
の要求を満たすことができます。
// ✅ OK
actor Counter: CustomStringConvertible {
let id: String
nonisolated var description: String {
"Counter with ID \(id)"
}
}
CustomStringConvertible
以外にも、例えば Identifiable
や Hashable
などほとんどの Swift の組み込みの protocol は同期的なプロパティやメソッドを要求するため、 actor を何らかの protocol に準拠させる時には nonisolated
を使う場面が多くなるでしょう。
まとめ
-
isolated
キーワードを関数のパラメータの actor につけることで、関数をその actor に isolate することができる -
nonisolated
キーワードを actor のメンバーにつけることで isolate しないことができる- actor 外から同期的にアクセスしたい場合や、 actor を特定の protocol に準拠させたいときに有効