48
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Swift] actor の isolated / nonisolated キーワードの使い方

Last updated at Posted at 2022-08-17

概要

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
}

この incrementCounterasync 関数ではありませんが、 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 されているせいで非同期にしかアクセスできず不便になっていることになります。このような状況で descriptionnonisolated をつけて 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 に準拠させたい時があります。例えば、 CounterCustomStringConvertible に準拠させたいとします。 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 が有効です。 descriptionnonisolated をつけることで actor 外からも同期的にアクセスできるようになるため、 CustomStringConvertible の要求を満たすことができます。

// ✅ OK
actor Counter: CustomStringConvertible {
    let id: String

    nonisolated var description: String {
        "Counter with ID \(id)"
    }
}

CustomStringConvertible 以外にも、例えば IdentifiableHashable などほとんどの Swift の組み込みの protocol は同期的なプロパティやメソッドを要求するため、 actor を何らかの protocol に準拠させる時には nonisolated を使う場面が多くなるでしょう。

まとめ

  • isolated キーワードを関数のパラメータの actor につけることで、関数をその actor に isolate することができる
  • nonisolated キーワードを actor のメンバーにつけることで isolate しないことができる
    • actor 外から同期的にアクセスしたい場合や、 actor を特定の protocol に準拠させたいときに有効

参考

48
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?