3

私は最近、Swift がSwift 5.5の Actor モデルで同時実行サポートを導入したことを知りました。このモデルにより、安全な同時実行コードを使用して、共有された変更可能な状態がある場合にデータ競合を回避できます。

アプリの UI でメイン スレッドのデータ競合を回避したいと考えています。このため、プロパティまたはスタイルDispatchQueue.main.asyncを設定する場所はどこでも呼び出しサイトでラップしています。UIImageView.imageUIButton

// Original function
func setImage(thumbnailName: String) {
    myImageView.image = UIImage(named: thumbnailName)
}

// Call site
DispatchQueue.main.async {
    myVC.setImage(thumbnailName: "thumbnail")
}

メイン キューでメソッドを手動でディスパッチすることを覚えておく必要があるため、これは安全ではないようです。他のソリューションは次のようになります。

func setImage(thumbnailName: String) {
   DispatchQueue.main.async {
      myImageView.image = UIImage(named: thumbnailName)
   }
}

しかし、これは多くのボイラープレートのように見えます。複数のレベルの入れ子を持つ複雑な関数にこれを使用するのが好きだとは言えません。

アクターの Swift サポートのリリースは、これに対する完璧なソリューションのように見えます。では、コードをより安全にする方法はありますか?つまり、アクターを使用してメイン スレッドで常に UI 関数を呼び出す方法はありますか?

4

1 に答える 1

10

アクターの分離と再入可能性が Swift stdlib に実装されるようになりました。そのため、Apple は、データ競合を回避するために、多くの新しい同時実行機能を備えた同時実行ロジックのモデルを使用することをお勧めします。ロックベースの同期 (多くのボイラープレート) の代わりに、よりクリーンな代替手段が用意されました。

とを含む一部のUIKitクラスは、すぐに使用できる のサポートを備えています。したがって、カスタム UI 関連のクラスでアノテーションを使用するだけで済みます。たとえば、上記のコードでは、自動的にメイン キューにディスパッチされます。ただし、View Controller の外部のメイン スレッドで呼び出しが自動的にディスパッチされることはありません。UIViewControllerUILabel@MainActormyImageView.imageUIImage.init(named:)

一般に、@MainActorは UI 関連の状態への同時アクセスに役立ち、手動でディスパッチすることもできますが、最も簡単に実行できます。以下に考えられる解決策を概説しました。

解決策 1

可能な限り単純。この属性は、UI 関連のクラスで役立ちます。Apple は、@MainActorメソッド アノテーションを使用してプロセスをよりクリーンにしました。

@MainActor func setImage(thumbnailName: String) {
    myImageView.image = UIImage(image: thumbnailName)
}

このコードは でラップするのと同じですDispatchQueue.main.asyncが、呼び出しサイトは次のようになります。

await setImage(thumbnailName: "thumbnail")

解決策 2

カスタム UI 関連のクラスがある場合は@MainActor、型自体に適用することを検討できます。これにより、すべてのメソッドとプロパティが main でディスパッチされDispatchQueueます。

nonisolated次に、非 UI ロジックのキーワードを使用して、メイン スレッドから手動でオプトアウトできます。

@MainActor class ListViewModel: ObservableObject {
    func onButtonTap(...) { ... }

    nonisolated func fetchLatestAndDisplay() async { ... }
}

内でawait呼び出すときに明示的に指定する必要はありません。onButtonTapactor

解決策 3 (関数だけでなくブロックでも機能する)

actorwithの外側のメイン スレッドで関数を呼び出すこともできます。

func onButtonTap(...) async {
    await MainActor.run { 
        ....
    }
}

別の内部actor

func onButtonTap(...) {
    await MainActor.run { 
        ....
    }
}

内から戻りたい場合はMainActor.run、署名でそれを指定するだけです。

func onButtonTap(...) async -> Int {
    let result = await MainActor.run { () -> Int in
        return 3012
    }
    return result
}

このソリューションは、上の関数全体をラップするのに最も適している上記の 2 つのソリューションよりもわずかにクリーンではありません。ただし、s間のインタースレッドコードも 1 つにできます(thx @Bill の提案)。MainActoractor.runactorfunc

解決策 4 (非非同期関数内で機能するブロック ソリューション)

@MainActor ソリューション 3でブロックをスケジュールする別の方法:

func onButtonTap(...) {
    Task { @MainActor in
        ....
    }
}

ここでのソリューション 3 に対する利点は、囲みをfuncとしてマークする必要がないことasyncです。ただし、これは解決策 3 のようにすぐにではなく、後でブロックをディスパッチすることに注意してください。

概要

アクターは、Swift コードをより安全に、よりクリーンに、そしてより簡単に記述できるようにします。過度に使用しないでください。ただし、UI コードをメイン スレッドにディスパッチすることは優れたユース ケースです。この機能はまだベータ版であるため、フレームワークは将来変更/改善される可能性があることに注意してください。

ボーナスノート

キーワードはorとactor同じ意味で簡単に使用できるため、同時実行が厳密に必要なインスタンスにのみキーワードを制限することをお勧めします。キーワードを使用すると、インスタンスの作成に余分なオーバーヘッドが追加されるため、管理する共有状態がない場合は意味がありません。classstruct

共有状態が必要ない場合は、不必要に作成しないでください。structインスタンスの作成は非常に軽量であるため、ほとんどの場合、新しいインスタンスを作成することをお勧めします。例えばSwiftUI

于 2021-06-08T19:23:00.330 に答える