LoginSignup
3
3

More than 1 year has passed since last update.

Swiftにおける`Actor`とはなにか

Posted at

1. Actorによって解決できること

複数スレッドでの非同期な作業を実装する場合、多くの場合で、キャッシュや結果などの共有の可変状態を参照するケースがあります。

これらは注意深く実装しないと、配列の更新中に別スレッドが配列にアクセスしてしまったり、真偽値の更新に失敗したりするなどの意図せぬ動きをすることがあります。

NSLockなどを注意深く用いることでこの問題を解決できますが、この解決方法はコストが高く、実装ミスなどにより容易に意図せぬ動きを発生させ、結果としてバグの原因となる可能性があります。

この問題を解決するのがActorです。

2. Actorの詳細

Actorは自分自身が管理する共有の可変状態への一貫した安全なアクセスを提供します。

このため、Actorの変数などにプログラムの他の要素が並列アクセスすることはありません。

Actorは通常の型と似た振る舞いをします。

Actorの目的は共有の可変状態を管理することのため、Actorはクラスと同じ参照タイプです。

下記のようなシンプルなコードでActorは定義でき、Actor定義であるために、valueへの並列アクセスは発生しません。

actor Wallet {
    var amountOf1000Yen: Int

    func add1000Yen() -> Int {
        amountOf1000Yen = amountOf1000Yen + 1

        return amountOf1000Yen
    }
}

ただしActorのコードはその性質上非同期になるので、呼び出し側は下記のように awiat を付与し非同期であることを表す必要があります

let counter = Wallet()

Task.detached {
    print(await counter.add1000Yen())
}

Task.detached {
    print(await counter.add1000Yen())
}

この問題は、ActorにProtocolに適合させる際に問題となりえます。

例えば、Wallet Actorを拡張し、WalletID型をもたせ、WalletをHashableプロトコルに準拠させてみましょう。

actor Wallet {
    var id: WalletID
    var amountOf1000Yen: Int

    func add1000Yen() -> Int {
        amountOf1000Yen = amountOf1000Yen + 1

        return amountOf1000Yen
    }
}

extension Wallet: Hashable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

実際には上記のコードはコンパイルできません。

なぜなら、hash関数の呼び出しは同期で行う必要があり、Actorへの同期呼び出しを許可してしまうと、Actorの内部状態の保護を継続できないからです。

この問題は、idlet にし、 hash関数の頭に nonisolated attributeを付与することで解決できます。

actor Wallet {
    let id: WalletID
    var amountOf1000Yen: Int

    func add1000Yen() -> Int {
        amountOf1000Yen = amountOf1000Yen + 1

        return amountOf1000Yen
    }
}

extension Wallet: Hashable {
    nonisolated func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

nonisolated attributeをつけた関数はActorの管理外とみなされるため、varで宣言されたプロパティにアクセスすることはできません。

3. 特別なActor

iOSアプリ開発において多くのUI処理は下記コードのようにMain Thread上で行う必要があります。

let userNameLabel = UILabel()

// 実行スレッドがMainThread以外の場合に備えてUIへの反映をMainThreadで非同期で行うように指定する
DispatchQueue.main.async {
    userNameLabel.text = "h1d3mun3"
    userNameLabel.isHidden = false
    view.addSubView(userNameLabel)
}

毎回 DispatchQueue.main.async と書くのは面倒くさいですし、何よりコードを書いている最中に実行スレッドの考慮がもれ、バックグラウンドでUIを操作してしまうかもしれません。

Actorのなかでも、処理がMainThread上で行われることが保証されているMainActorというものがあります。

上記のサンプルをMainActorで書き直してみると下記のようになります

let userNameLabel = UILabel()

@MainActor func updateUserNameLabel(to userName: String?) {
    userNameLabel.isHidden = (userName == nil)
    guard let userName = userName else { return }

    userNameLabel.text = userName
}

await updateUserNameLabel(to: "h1d3mun3")

この @MainActor Attributeは関数だけではなく下記のようにクラスにも適合させることができます

@MainActor final class SomeUsefullMainThreaViewController: UIViewController { .... }

4. おわりに

同時に発表された async/await と併用することで非同期処理をよりかんたんに、そしてより安全に記述することができます。

私の関わるプロダクトにおいても、随時検討を進めた上で導入し、より安全なプロダクトにしていきたいと考えています。

3
3
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
3
3