Swift Concurrency
[swift] メインスレッドから処理を逃すために Task.detached を使う必要はない(ことが多い)
- async 関数はあえて main actor に isolate されていない限り必ずバックグラウンドスレッドで実行されます
func someAsyncFunction() async {
// main actor に isolate されていない async 関数なので、
// どこから呼ばれたかに関わらずバックグラウンドで実行される
}
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
// ここは main actor だが、 someAsyncfunction の中身は
// バックグラウンドスレッドで実行される
await someAsyncFunction()
}
}
}
- 注意: sync関数は呼び出し元のスレッドで実行される
func someSuperHeavyComputation() {
// sync 関数なので呼び出し元のスレッドで実行される
// すごく重い計算を行うので、メインスレッドから呼び出されると
// メインスレッドを占有して UI のレスポンスに問題が発生する
}
final class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
Task {
await someAsyncFunction() // 中身がバックグラウンドスレッドで実行される
someSuperHeavyComputation() // ❗️ 中身がメインスレッドで実行開始されてしまう
}
Task.detached {
await someAsyncFunction() // 中身がバックグラウンドスレッドで実行される
someSuperHeavyComputation() // ✅ 中身がバックグラウンドスレッドで実行開始される
}
}
}
- Task Local Value: 親子間のタスクで共有可能な値
- Task.detachedではtask local valueを引き継がない
Task Local Valueを使う機会は基本なさそう。
Swift のアクター: データ競合の使用方法と防止方法
-
Swift のアクターはデータ競合を完全に解決することを目指していますが、依然としてデータ競合が発生する可能性がある
-
データ競合は、同期せずに複数のスレッドから同じメモリにアクセスし、少なくとも 1 回のアクセスが書き込みである場合に発生
-
データ競合は、予測できない動作、メモリ破損、不安定なテスト、および奇妙なクラッシュを引き起こす可能性があります
- いつ発生するか、どのように再現するか、理論に基づいて修正する方法がわからないため、今日は解決できないクラッシュが発生する可能性があります
-
Swift のアクターはその状態をデータ競合から保護し、これを使用すると、アプリケーションの作成中にコンパイラーが役立つフィードバックを提供できるようになる
-
Swift コンパイラーは、アクターに伴う制限を静的に強制し、可変データへの同時アクセスを防止できます
-
アクターは参照型
-
クラスと比べた重要な違い
- 継承をサポートしていない
- 継承をサポートしないということは、利便性や必須の初期化子、オーバーライド、クラス メンバー、およびfinalステートメント などの機能が必要ない
- 継承をサポートしていない
アクターが同期を使用してデータ競合を防ぐ方法
- アクターは、分離されたデータへの同期アクセスを作成することでデータ競合を防ぎます
- アクターが登場する前は、あらゆる種類のロックを使用して同じ結果を作成していました。このようなロックの例としては、書き込みアクセスを処理するためのバリアと組み合わせた同時ディスパッチ キューが挙げられます。
final class ChickenFeederWithQueue {
let food = "worms"
/// A combination of a private backing property and a computed property allows for synchronized access.
private var _numberOfEatingChickens: Int = 0
var numberOfEatingChickens: Int {
queue.sync {
_numberOfEatingChickens
}
}
/// A concurrent queue to allow multiple reads at once.
private var queue = DispatchQueue(label: "chicken.feeder.queue", attributes: .concurrent)
func chickenStartsEating() {
/// Using a barrier to stop reads while writing
queue.sync(flags: .barrier) {
_numberOfEatingChickens += 1
}
}
func chickenStopsEating() {
/// Using a barrier to stop reads while writing
queue.sync(flags: .barrier) {
_numberOfEatingChickens -= 1
}
}
}
-
ここには保守する必要のあるコードがかなり多くあります。スレッドセーフではないデータにアクセスする場合は、キューを使用することについて慎重に検討する必要があります。読み取りを一時停止して書き込みを許可するには、バリア フラグが必要です。繰り返しますが、コンパイラは強制しないため、これを自分で処理する必要があります。最後に、ここでは を使用していますDispatchQueueが、どのロックを使用するのが最適かについてはよく議論されます。
-
アクターを使用すると、Swift は同期アクセスを可能な限り最適化できます
- インスタンスがはるかにシンプルで読みやすい
- アクセスの同期に関連するすべてのロジックは、Swift 標準ライブラリ内の実装の詳細として隠されています
actor ChickenFeeder {
let food = "worms"
var numberOfEatingChickens: Int = 0
func chickenStartsEating() {
numberOfEatingChickens += 1
}
func chickenStopsEating() {
numberOfEatingChickens -= 1
}
}
-
ただし、最も興味深い部分は、変更可能なプロパティとメソッドのいずれかを使用または読み取りしようとしたときに発生します
-
可変プロパティにアクセスするときにも同じことが起こります
-
不変プロパティの読み取りはスレッドセーフなので許可されている
- 読み取り中に別のスレッドから値を変更できないため、データ競合のリスクはない
let feeder = ChickenFeeder()
print(feeder.food)
- ただし、他のメソッドとプロパティは、参照型の可変状態を変更します。データ競合を防ぐには、順次アクセスを許可する同期アクセスが必要です
async/await を使用してアクターからのデータにアクセスする
アクセスがいつ許可されるかが不明なため、アクターの可変データへの非同期アクセスを作成する必要があります。データにアクセスする他のスレッドがない場合は、直接アクセスを取得します。ただし、別のスレッドが可変データへのアクセスを実行している場合は、アクセスが許可されるまで座って待つ必要があります
Swift では、次のawaitキーワードを使用して非同期アクセスを作成できます
let feeder = ChickenFeeder()
await feeder.chickenStartsEating()
print(await feeder.numberOfEatingChickens) // Prints: 1
- アクター内のメソッドはデフォルトで分離されており(=同時アクセスできない)、不変プロパティにのみアクセスする場合でもawaitをつける必要がある
let feeder = ChickenFeeder()
await feeder.printWhatChickensAreEating()
- そこで、分離アクセスが不要な場合(=同期アクセスが必要なものにアクセスしない場合)にはnonisolatedをつければawaitをつける必要がなくなる
- nonisolatedメソッドが分離されたデータにアクセスしていないことを Swift コンパイラーに伝える
- nonisolatedキーワードは計算されたプロパティにも使用できる
extension ChickenFeeder {
nonisolated func printWhatChickensAreEating() {
print("Chickens are eating \(food)")
}
}
let feeder = ChickenFeeder()
feeder.printWhatChickensAreEating()
extension ChickenFeeder: CustomStringConvertible {
nonisolated var description: String {
"A chicken feeder feeding \(food)"
}
}
アクターの使用時にデータ競合が依然として発生する理由
- コード内でアクターを一貫して使用すると、データ競合が発生するリスクが確実に軽減される
- しかし、以下のように2 つのスレッドが await を使用してアクターのデータにアクセスすると、分離アクセスはされるが、「どのスレッドが最初に分離アクセスを開始するか?」という競合状態が発生する
queueOne.async {
await feeder.chickenStartsEating()
}
queueTwo.async {
print(await feeder.numberOfEatingChickens)
}
- 上記ではスレッドの実行順序によって2つの結果が発生し得る(予期しない動作が発生)
- 1 番目の列を先頭にして、食べる鶏の数を増やします。キュー 2 は 1 を出力します
- キュー 2 が先頭で、まだ 0 である食べている鶏の数を出力します。
結論
Swift アクターは、Swift で書かれたアプリケーションでよくある問題であるデータ競合を解決します。可変データは同期的にアクセスされるため、安全性が確保されます。
Swift での MainActor の使用法をメインスレッドへのディスパッチについて
- MainActor は、メイン スレッドでタスクを実行するエグゼキュータを提供するグローバル アクター
- アプリを構築するときは、メイン スレッドで UI 更新タスクを実行することが不可欠ですが、@MainActor属性を使用すると、UI がメインスレッドで常に更新されるようになります
- MainActor は、メイン スレッドでタスクを実行するグローバルにユニークなアクターです。これをプロパティ、メソッド、インスタンス、クロージャに使用して、メインスレッドでタスクを実行できます。
グローバルアクターを理解する
コード内で MainActor を使用する方法に入る前に、グローバル アクターの概念を理解することが重要です。グローバル アクターはシングルトンとして表示されます。インスタンスは 1 つだけ存在します。グローバル アクターは次のように定義できます。
@globalActor
actor SwiftLeeActor {
static let shared = SwiftLeeActor()
}
共有プロパティはプロトコルの要件でありGlobalActor、グローバルに一意のアクター インスタンスを持つことが保証されます。定義したら、他のアクターの場合と同様に、プロジェクト全体でグローバル アクターを使用できます。
@SwiftLeeActor
final class SwiftLeeFetcher {
// ..
}
グローバル アクター属性を使用する場合はどこでも、共有アクター インスタンスを通じて同期が確保され、宣言への相互排他的アクセスが確保されます。「Swift のアクター: データ競合の使用方法と防止方法」で説明されているように、結果は一般的なアクターと同様です。
基礎となる@MainActor実装は、カスタム定義されたものと似ています
@globalActor
final actor MainActor: GlobalActor {
static let shared: MainActor
}
これはデフォルトで利用可能であり、同時実行フレームワーク内で定義されています。つまり、このグローバル アクターの使用をすぐに開始し、このグローバル アクターを介して同期することで、メイン スレッドで実行されるようにコードをマークできます。
- MainActor.run {}を使うと、グローバル アクター属性を使用して本体を定義しなかった場合でも、メソッド内から MainActor を直接使用できるようになります
- 言い換えれば、DispatchQueue.main.asyncもう使う必要はありません。
- ただし、グローバル属性を使用してメインスレッドへのアクセスを制限することをお勧めします。グローバル アクター属性がないと、MainActor.runの使用を忘れてしまい、バックグラウンド スレッドで UI の更新が行われる可能性があります。
extension MainActor {
/// Execute the given body closure on the main actor.
public static func run<T>(resultType: T.Type = T.self, body: @MainActor @Sendable () throws -> T) async rethrows -> T
}
Task {
await someHeavyBackgroundOperation()
await MainActor.run {
// Perform UI updates
}
}
MainActor 属性はいつ使用する必要がありますか?
Swift 5.5 より前では、タスクがメインスレッドで実行されるようにするために、多くのディスパッチ ステートメントを定義していた可能性があります。例は次のようになります。
func fetchImage(for url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data, let image = UIImage(data: data) else {
DispatchQueue.main.async {
completion(.failure(ImageFetchingError.imageDecodingFailed))
}
return
}
DispatchQueue.main.async {
completion(.success(image))
}
}.resume()
}
上記の例では、画像をメインスレッドに返すためにディスパッチが必要であることがわかります。いくつかの場所でディスパッチを実行する必要があるため、複数のクロージャがありコードが乱雑になります。
場合によっては、すでにメインスレッド上にあるときにメインキューにディスパッチすることもあります。このような場合、スキップできたはずの追加のディスパッチが発生することになります。
async/await とメイン アクターを使用するようにコードを書き直すことで、必要な場合にのみ最適化をディスパッチできるようになります。
このような場合、プロパティ、メソッド、インスタンス、またはクロージャをメイン アクターに分離することで、タスクがメイン スレッドで確実に実行されるようになります。理想的には、上記の例を次のように書き換えます。
@MainActor
func fetchImage(for url: URL) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageFetchingError.imageDecodingFailed
}
return image
}
この@MainActor属性により、ネットワーク リクエストがバックグラウンド キューで実行されている間、ロジックがメイン スレッドで実行されることが保証されます。
@MainActorをつけていてもバックグランドスレッドで実行されてしまうケース
よくある勘違い: メソッドが に@MainActor属性付けされている場合、常にメインスレッドで実行されると想定していること
たとえば、次を使用してバックグラウンド スレッドにディスパッチする既存のコードがあるとしますDispatchQueue。
DispatchQueue.global().async {
/// Executed on some kind of background thread.
/// In this case, simulated by calling into DispatchQueue.global().
let dispatcher = ActorDispatcher()
dispatcher.methodAttributedWithMainActor()
}
上記は明示的な例ですが、Foundation などのフレームワークにおける Apple の API の多くは、気付かないうちにバックグラウンド スレッドで返されます。コードはコンパイルされますが、コンパイラーはタスクなどの使用に関する変更を提案しません。したがって、コードはスレッドセーフであり、メインスレッドで適切に実行されると想定します。アプリを一時停止してデバッグ ナビゲーターを調べる場合は、その逆が当てはまります。
上記のDispatchQueueクロージャは、非分離コンテキストで同期的に実行されます。コンパイラーは、潜在的な障害について確信がある場合にのみ変更を提案でき、同時実行を意識しないコードではアクターの分離を強制しません。言い換えれば、コンパイルエラーがない場合、コードが宛先アクター上で実行されると想定することはできません。
Swift Concurrency Checkingにより防止可能
非分離および分離されたキーワード: アクターの分離について
アクター分離制御の追加の一環として、非分離キーワードと分離キーワードが導入されました。アクターは、新しい同時実行フレームワークで共有の可変状態の同期を提供する新しい方法です。
Sendable クロージャと @Sendable クロージャをコード例で説明
Sendable
ある型について、その public な API が並列に実行されても安全である (≒ [Data race] を生じさせない) ことが保証されているのであれば、[Sendable] に適合することを検討できる。値型はもちろん、内部的に [Lock] による同期機構を持った class や、mutator を持たない immutable な class なども、[Sendable] への適合を検討して良い。
ただし、安全であることが保証されていないにも関わらず [Sendable] に適合させた場合には [Data race] を生じさせる恐れがあるので注意する。
Sendable
所感:
- Sendableは単なるデータ競合が起きないことを保証するマーカーであり、何か機能を与えるものではない
- Sendableをつけられるということは、データ競合が起きないことが保証される(警告をなくせば)
- Sendableをつけられるもの
- 値のタイプ
- 可変ストレージを持たない参照型
- 状態へのアクセスを内部で管理する参照型
- 関数とクロージャ ( でマーク@Sendable)
- Sendableをつけられるもの
- Sendableをつけられるということは、データ競合が起きないことが保証される(警告をなくせば)
警告出る場合
警告でない場合
- データ競合が起きないことが保証されていない値はSendableをつけて保証しないと、actor間で値を渡すことができない
たとえ自分でActorを定義していなくても、Swift Concurrency Checkingで、
標準APIでMainActorがついていたり@Sendableクロージャーに対して、データを渡す際にSendableがついてないとエラーが出るということ?
Sendableクロージャーは、acotrのメソッドの引数で使われる?