同時実行の説明:マルチスレッド iOS アプリを構築する方法
iOS での同時実行は大きなトピックです。そこで、この記事では、キューと Grand Central Dispatch (GCD) フレームワークに関するサブトピックにズームインしたいと思います。
特に、シリアル キューと並行キューの違い、および同期実行と非同期実行の違いを調べたいと思います。
これまで GCD を使用したことがない場合は、この記事から始めることをお勧めします。 GCD の経験があり、上記のトピックにまだ興味がある場合は、それでも役立つと思います。そして、途中で新しいことを 1 つまたは 2 つ取り上げていただければ幸いです。
この記事の概念を視覚的に示すために、SwiftUI コンパニオン アプリを作成しました。このアプリには楽しい短いクイズもあり、この記事を読む前後に試してみることをお勧めします。ここからソース コードをダウンロードするか、ここからパブリック ベータ版を入手してください。
まず GCD の紹介から始め、次に同期、非同期、シリアル、およびコンカレントについて詳しく説明します。その後、並行処理を行う際の落とし穴について説明します。最後に、要約と一般的なアドバイスで締めくくります。
はじめに
まず、GCD とディスパッチ キューの簡単な紹介から始めましょう。 同期と非同期に進んでください。 トピックに既に精通している場合は、セクションを参照してください。
同時実行とグランド セントラル ディスパッチ
同時実行により、デバイスに複数の CPU コアがあるという事実を利用できます。これらのコアを利用するには、複数のスレッドを使用する必要があります。ただし、スレッドは低レベルのツールであり、スレッドを手動で効率的に管理することは非常に困難です。
Grand Central Dispatch は、開発者がスレッド自体を手動で作成および管理することなくマルチスレッド コードを記述できるようにするための抽象化として、10 年以上前に Apple によって作成されました。
GCD で、Apple は非同期設計アプローチを採用しました 問題に。スレッドを直接作成する代わりに、GCD を使用して作業タスクをスケジュールすると、システムがリソースを最大限に活用してこれらのタスクを実行します。 GCD は必要なスレッドの作成を処理し、それらのスレッドでタスクをスケジュールして、スレッド管理の負担を開発者からシステムに移します。
GCD の大きな利点は、並行コードを作成するときにハードウェア リソースについて心配する必要がないことです。 GCD はスレッド プールを管理し、シングルコアの Apple Watch からメニーコアの MacBook Pro まで拡張します。
ディスパッチ キュー
これらは、定義した一連のパラメーターを使用して任意のコード ブロックを実行できる GCD の主要な構成要素です。ディスパッチ キュー内のタスクは、常に先入れ先出し (FIFO) 方式で開始されます。 開始したと言ったことに注意してください 、タスクの完了時間はいくつかの要因に依存し、FIFO であるとは限りません (詳細は後述)。
大まかに言えば、3 種類のキューを利用できます。
- メイン ディスパッチ キュー (シリアル、定義済み)
- グローバル キュー (同時、事前定義)
- プライベート キュー (シリアルまたはコンカレントにすることができます。ユーザーが作成します)
すべてのアプリには、シリアル であるメイン キューが付属しています メインスレッドでタスクを実行するキュー。このキューは、アプリケーションの UI を描画し、ユーザーの操作 (タッチ、スクロール、パンなど) に応答する役割を果たします。このキューを長時間ブロックすると、iOS アプリがフリーズしたように見え、macOS アプリで悪名高いビーチが表示されます。ボール/糸車。
長時間実行されるタスク (ネットワーク呼び出し、計算負荷の高い作業など) を実行する場合、この作業をバックグラウンド キューで実行することにより、UI のフリーズを回避します。次に、メイン キューの結果で UI を更新します。
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
DispatchQueue.main.async { // UI work
self.label.text = String(data: data, encoding: .utf8)
}
}
}
経験則として、すべての UI 作業はメイン キューで実行する必要があります。 Xcode で Main Thread Checker オプションをオンにして、UI 作業がバックグラウンド スレッドで実行されるたびに警告を受け取ることができます。
メイン キューに加えて、すべてのアプリには、さまざまなレベルのサービス品質 (GCD における優先度の抽象的な概念) を持つ事前定義された複数の同時キューが付属しています。
たとえば、ユーザー インタラクティブに作業を非同期で送信するコードは次のとおりです。 (最優先) QoS キュー:
DispatchQueue.global(qos: .userInteractive).async {
print("We're on a global concurrent queue!")
}
または、デフォルトの優先度を呼び出すこともできます 次のように QoS を指定しないことによるグローバル キュー:
DispatchQueue.global().async {
print("Generic global queue")
}
さらに、次の構文を使用して独自のプライベート キューを作成できます:
let serial = DispatchQueue(label: "com.besher.serial-queue")
serial.async {
print("Private serial queue")
}
プライベート キューを作成するときは、Xcode のナビゲーター、lldb、Instruments でデバッグする際に役立つため、説明的なラベル (逆引き DNS 表記など) を使用すると役立ちます。
デフォルトでは、プライベート キューはシリアルです (これが何を意味するかはすぐに説明します、約束してください!) プライベート concurrent オプションの attributes を介して行うことができます パラメータ:
let concurrent = DispatchQueue(label: "com.besher.serial-queue", attributes: .concurrent)
concurrent.sync {
print("Private concurrent queue")
}
オプションの QoS パラメータもあります。作成したプライベート キューは、最終的に、指定されたパラメーターに基づいて、グローバル同時実行キューの 1 つに配置されます。
タスクの内容
タスクをキューにディスパッチすることについて説明しました。タスクは、sync
を使用してキューに送信する任意のコード ブロックを参照できます。 または async
機能。それらは匿名の閉鎖の形で提出することができます:
DispatchQueue.global().async {
print("Anonymous closure")
}
または、後で実行されるディスパッチ作業項目内:
let item = DispatchWorkItem(qos: .utility) {
print("Work item to be executed later")
}
ディスパッチを同期または非同期のどちらで行うかに関係なく、シリアル キューまたは同時キューのどちらを選択しても、1 つのタスク内のすべてのコードが 1 行ずつ実行されます。並行性は、multiple を評価する場合にのみ関連します
たとえば、同じの中に 3 つのループがあるとします。 これらのループは常に 順番に実行:
DispatchQueue.global().async {
for i in 0..<10 {
print(i)
}
for _ in 0..<10 {
print("?")
}
for _ in 0..<10 {
print("?")
}
}
このコードは常に 0 から 9 までの 10 桁の数字を出力し、その後に 10 個の青い円、10 個の壊れたハートが続きます。
個々のタスクも独自の QoS レベルを持つことができます (デフォルトでは、タスクはキューの優先度を使用します)。このキュー QoS とタスク QoS の違いは、優先度の逆転セクションで説明するいくつかの興味深い動作につながります。
ここまでで、シリアルとは何だろうと思っているかもしれません。 および同時 すべてについてです。 sync
の違いについても疑問に思われるかもしれません。 と async
タスクを送信するとき。これでこの記事の核心にたどり着きますので、詳しく見ていきましょう!
同期と非同期
タスクをキューにディスパッチするときは、sync
を使用して、同期または非同期のどちらでディスパッチするかを選択できます。 および async
発送機能。同期と非同期は主にソースに影響します 提出されたタスクの、それが提出されているキューです .
コードが sync
に達したとき ステートメントを使用すると、そのタスクが完了するまで現在のキューがブロックされます。タスクが戻る/完了すると、制御が呼び出し元に返され、sync
に続くコードが タスクは続行されます。
sync
を考えてみてください 「ブロッキング」と同義。
async
一方、ステートメントは現在のキューに対して非同期に実行され、async
の内容を待たずにすぐに制御を呼び出し元に返します。 実行する閉鎖。その非同期クロージャー内のコードがいつ実行されるかについての保証はありません。
現在のキュー?
ソースが何であるか、または現在が明らかでない場合があります 、キューは、コードで常に明示的に定義されているわけではないためです。
たとえば、sync
を呼び出す場合 viewDidLoad 内のステートメントでは、現在のキューがメイン ディスパッチ キューになります。 URLSession 完了ハンドラー内で同じ関数を呼び出すと、現在のキューはバックグラウンド キューになります。
同期と非同期の話に戻り、次の例を見てみましょう:
DispatchQueue.global().sync {
print("Inside")
}
print("Outside")
// Console output:
// Inside
// Outside
上記のコードは、現在のキューをブロックし、クロージャーに入り、「外側」を出力する前に「内側」を出力することによってグローバル キューでそのコードを実行します。この順序は保証されています。
async
を試すとどうなるか見てみましょう 代わりに:
DispatchQueue.global().async {
print("Inside")
}
print("Outside")
// Potential console output (based on QoS):
// Outside
// Inside
コードはクロージャをグローバル キューに送信し、すぐに次の行の実行に進みます。 おそらく 「内側」の前に「外側」を印刷しますが、この順序は保証されません。これは、ソース キューと宛先キューの QoS、およびシステムが制御するその他の要因によって異なります。
スレッドは GCD の実装の詳細です — スレッドを直接制御することはできず、キューの抽象化を使用してのみ処理できます。それにもかかわらず、GCD で遭遇する可能性のあるいくつかの課題を理解するために、スレッドの動作を「隠れてのぞく」ことは役立つと思います。
たとえば、sync
を使用してタスクを送信する場合 、GCD は現在のスレッド (呼び出し元) でそのタスクを実行することによってパフォーマンスを最適化します。
ただし、1 つ例外があります。それは、同期タスクをメイン キューに送信する場合です。 — こうすると、呼び出し元ではなく、常にメイン スレッドでタスクが実行されます。この動作は、優先順位の逆転セクションで検討するいくつかの影響を与える可能性があります。
どれを使う?
作業をキューに送信する場合、Apple は同期実行よりも非同期実行を使用することをお勧めします。ただし、sync
の状況があります。 競合状態を処理する場合や非常に小さなタスクを実行する場合など、より良い選択かもしれません。これらの状況については後ほど説明します。
関数内で作業を非同期に実行することの大きな結果の 1 つは、関数がその値を直接返すことができなくなることです (実行中の非同期作業に依存している場合)。代わりにクロージャ/コンプリーション ハンドラ パラメータを使用して結果を配信する必要があります。
この概念を実証するために、画像データを受け取り、負荷の高い計算を実行して画像を処理し、結果を返す小さな関数を取り上げてみましょう:
func processImage(data: Data) -> UIImage? {
guard let image = UIImage(data: data) else { return nil }
// calling an expensive function
let processedImage = upscaleAndFilter(image: image)
return processedImage
}
この例では、関数 upscaleAndFilter(image:)
数秒かかる場合があるため、UI がフリーズしないように別のキューにオフロードする必要があります。画像処理専用のキューを作成し、高価な関数を非同期でディスパッチしましょう:
let imageProcessingQueue = DispatchQueue(label: "com.besher.image-processing")
func processImageAsync(data: Data) -> UIImage? {
guard let image = UIImage(data: data) else { return nil }
imageProcessingQueue.async {
let processedImage = upscaleAndFilter(image: image)
return processedImage
}
}
このコードには 2 つの問題があります。まず、return ステートメントは async クロージャー内にあるため、processImageAsync(data:)
に値を返さなくなりました。 機能し、現在は何の役にも立たない。
しかし、より大きな問題は、私たちの processImageAsync(data:)
async
に入る前に関数が本体の最後に到達したため、関数は値を返さなくなりました。 閉鎖。
このエラーを修正するには、関数が直接値を返さないように調整します。代わりに、非同期関数が作業を完了した後に呼び出すことができる新しい完了ハンドラー パラメーターがあります。
let imageProcessingQueue = DispatchQueue(label: "com.besher.image-processing")
func processImageAsync(data: Data, completion: @escaping (UIImage?) -> Void) {
guard let image = UIImage(data: data) else {
completion(nil)
return
}
imageProcessingQueue.async {
let processedImage = self.upscaleAndFilter(image: image)
completion(processedImage)
}
}
この例で明らかなように、関数を非同期にする変更は呼び出し元に反映され、呼び出し元はクロージャーを渡し、結果も非同期で処理する必要があります。非同期タスクを導入すると、複数の関数のチェーンを変更する可能性があります。
同時実行と非同期実行は、今見たようにプロジェクトを複雑にします。この間接化により、デバッグがより困難になります。そのため、設計の早い段階で並行性について検討することが本当に効果的です。設計サイクルの最後に追加したいものではありません.
対照的に、同期実行では複雑さが増しません。むしろ、以前と同じように return ステートメントを引き続き使用できます。 sync
を含む関数 タスクは、そのタスク内のコードが完了するまで返されません。したがって、完了ハンドラは必要ありません。
小さなタスク (値の更新など) を送信する場合は、同期的に実行することを検討してください。これはコードをシンプルに保つのに役立つだけでなく、パフォーマンスも向上します。」—「非同期は、完了するまでに 1 ミリ秒未満かかる小さなタスクの作業を非同期で実行する利点を上回るオーバーヘッドを招くと考えられています。
ただし、上記で実行した画像処理のように大きなタスクを送信する場合は、呼び出し元を長時間ブロックしないように非同期で実行することを検討してください。
同じキューでのディスパッチ
タスクをキューからそれ自体に非同期的にディスパッチすることは安全ですが (たとえば、現在のキューで .asyncAfter を使用できます)、タスクを同期的にディスパッチすることはできません。 キューから同じキューに。これを行うと、アプリがすぐにクラッシュするデッドロックが発生します!
この問題は、元のキューに戻る一連の同期呼び出しを実行するときに発生する可能性があります。つまり、あなたは sync
タスクを別のキューに追加し、タスクが完了すると、結果を元のキューに同期して戻すため、デッドロックが発生します。 async
を使用 このようなクラッシュを避けるために。
メイン キューのブロック
からタスクを同期的にディスパッチする メイン キューがそのキューをブロックするため、タスクが完了するまで UI がフリーズします。したがって、本当に軽い作業を行っている場合を除き、メイン キューから作業を同期的にディスパッチすることは避けたほうがよいでしょう。
シリアルと同時接続
シリアル および同時 目的地{/に影響を与える — 作業が実行のために送信されたキュー。これは sync とは対照的です および非同期 、ソースに影響を与えました .
シリアル キューは、そのキューでディスパッチするタスクの数に関係なく、一度に複数のスレッドで作業を実行しません。その結果、タスクは先入れ先出しの順序で開始されるだけでなく、終了することが保証されます。
さらに、シリアルキューをブロックすると (sync
を使用) 呼び出し、セマフォ、またはその他のツール)、そのキューでのすべての作業は、ブロックが終了するまで停止します。
並行キューは複数のスレッドを生成でき、システムは作成されるスレッドの数を決定します。タスクは常に開始 ただし、キューは次のタスクを開始する前にタスクの終了を待機しないため、同時キューのタスクは任意の順序で終了できます。
並行キューでブロッキング コマンドを実行すると、このキューの他のスレッドはブロックされません。さらに、同時キューがブロックされると、スレッド爆発のリスクがあります .これについては後で詳しく説明します。
アプリのメイン キューはシリアルです。すべての事前定義されたグローバル キューは同時実行されます。作成するすべてのプライベート ディスパッチ キューは、デフォルトではシリアルですが、前述のようにオプションの属性を使用してコンカレントに設定できます。
serial の概念に注意することが重要です。 vs 同時 特定のキューについて説明する場合にのみ関連します。すべてのキューは 互いに相対的に同時です .
つまり、メイン キューからプライベート serial に作業を非同期的にディスパッチする場合 キュー、その作業は同時に完了します メインキューに関して。また、2 つの異なるシリアル キューを作成し、そのうちの 1 つでブロッキング作業を実行しても、もう 1 つのキューは影響を受けません。
複数のシリアル キューの同時実行性を示すために、次の例を見てみましょう:
let serial1 = DispatchQueue(label: "com.besher.serial1")
let serial2 = DispatchQueue(label: "com.besher.serial2")
serial1.async {
for _ in 0..<5 { print("?") }
}
serial2.async {
for _ in 0..<5 { print("?") }
}
ここでの両方のキューはシリアルですが、互いに関連して同時に実行されるため、結果はごちゃ混ぜになります。それらがそれぞれシリアル (またはコンカレント) であるという事実は、この結果には影響しません。 QoS レベルによって、誰が一般的に 最初に終了します (順序は保証されません)。
2 番目のループを開始する前に最初のループを確実に終了させたい場合は、最初のタスクを呼び出し元から同期的に送信できます。
let serial1 = DispatchQueue(label: "com.besher.serial1")
let serial2 = DispatchQueue(label: "com.besher.serial2")
serial1.sync { // <---- we changed this to 'sync'
for _ in 0..<5 { print("?") }
}
// we don't get here until first loop terminates
serial2.async {
for _ in 0..<5 { print("?") }
}
最初のループの実行中に呼び出し元をブロックしているため、これは必ずしも望ましいことではありません。
呼び出し元のブロックを回避するために、両方のタスクを非同期で送信できますが、同じ シリアル キュー:
let serial = DispatchQueue(label: "com.besher.serial")
serial.async {
for _ in 0..<5 { print("?") }
}
serial.async {
for _ in 0..<5 { print("?") }
}
caller に関してタスクが同時に実行されるようになりました 、順序をそのまま維持しながら。
オプションのパラメーターを使用して単一のキューを同時に実行すると、予想どおり、ごちゃごちゃした結果に戻ることに注意してください。
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
concurrent.async {
for _ in 0..<5 { print("?") }
}
concurrent.async {
for _ in 0..<5 { print("?") }
}
同期実行と逐次実行を混同することがありますが (少なくとも私はそうでした)、これらはまったく異なるものです。たとえば、3 行目の最初のディスパッチを前の例から sync
に変更してみてください。 コール:
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
concurrent.sync {
for _ in 0..<5 { print("?") }
}
concurrent.async {
for _ in 0..<5 { print("?") }
}
突然、結果が完全な順序に戻りました。しかし、これは同時キューです。 sync
でしたか ステートメントはどうにかしてそれをシリアル キューに変えますか?
答えはいいえです。
これは少し卑劣です。 async
に達しなかったことが原因です 最初のタスクの実行が完了するまで呼び出します。キューはまだ非常に並行していますが、コードの拡大されたセクション内にあります。まるでシリアルのようです。これは、最初のタスクが完了するまで呼び出し元をブロックし、次のタスクに進まないためです。
アプリの別の場所にある別のキューがまだ sync
を実行しているときに、この同じキューに作業を送信しようとした場合 声明、その仕事する ここで実行したものと並行して実行されます。これはまだ並行キューであるためです。
どれを使う?
シリアル キューは、CPU の最適化とキャッシュを利用して、コンテキストの切り替えを減らすのに役立ちます。
Apple では、アプリのサブシステムごとに 1 つのシリアル キューから始めることをお勧めします。たとえば、ネットワーク用に 1 つ、ファイル圧縮用に 1 つなどです。必要に応じて、setTarget メソッドまたはオプションのターゲットを使用して、後でサブシステムごとにキューの階層に拡張できます。キューを構築するときのパラメーター。
パフォーマンスのボトルネックが発生した場合は、アプリのパフォーマンスを測定し、同時キューが役立つかどうかを確認してください。測定可能なメリットが見られない場合は、シリアル キューに固執することをお勧めします。
落とし穴
優先順位の逆転とサービスの質
優先度の逆転とは、優先度の高いタスクが優先度の低いタスクによって実行されないようにすることであり、相対的な優先度が効果的に逆転します。
この状況は、高 QoS キューが低 QoS キューとリソースを共有し、低 QoS キューがそのリソースをロックする場合によく発生します。
しかし、私たちの議論にもっと関連する別のシナリオを取り上げたいと思います。それは、タスクを低 QoS シリアル キューに送信し、次に高 QoS タスクを同じキューに送信する場合です。高 QoS タスクは低 QoS タスクが終了するのを待たなければならないため、このシナリオでは優先度の逆転も発生します。
GCD は、優先度の高いタスクの「先」またはブロックしている優先度の低いタスクを含むキューの QoS を一時的に引き上げることで、優先度の逆転を解決します。
車が前で立ち往生しているようなものです の 救急車。突然、救急車が移動できるように赤信号を渡ることが許可されます (実際には、車は横に移動しますが、狭い (連続した) 通りか何かを想像してみてください。要点がわかります :-P)
反転の問題を説明するために、次のコードから始めましょう:
enum Color: String {
case blue = "?"
case white = "⚪️"
}
func output(color: Color, times: Int) {
for _ in 1...times {
print(color.rawValue)
}
}
let starterQueue = DispatchQueue(label: "com.besher.starter", qos: .userInteractive)
let utilityQueue = DispatchQueue(label: "com.besher.utility", qos: .utility)
let backgroundQueue = DispatchQueue(label: "com.besher.background", qos: .background)
let count = 10
starterQueue.async {
backgroundQueue.async {
output(color: .white, times: count)
}
backgroundQueue.async {
output(color: .white, times: count)
}
utilityQueue.async {
output(color: .blue, times: count)
}
utilityQueue.async {
output(color: .blue, times: count)
}
// next statement goes here
}
スターター キューを作成します (タスクを送信する場所 from )、および異なる QoS を持つ 2 つのキュー。次に、これら 2 つのキューのそれぞれにタスクをディスパッチし、各タスクは特定の色の円を同数印刷します (ユーティリティ キュー 青、背景 は白です。)
これらのタスクは非同期で送信されるため、アプリを実行するたびに、わずかに異なる結果が表示されます。ただし、ご想像のとおり、QoS が低い (バックグラウンド) キューは、ほとんどの場合、最後に終了します。実際、通常、最後の 10 ~ 15 個の円はすべて白です。
ただし、同期を送信するとどうなるか見てください 最後の async ステートメントの後にタスクをバックグラウンド キューに追加します。 sync
内に何も出力する必要さえありません。 この行を追加するだけで十分です:
// add this after the last async statement,
// still inside starterQueue.async
backgroundQueue.sync {}
コンソールの結果が反転しました!これで、優先度の高いキュー (ユーティリティ) が常に最後に終了し、最後の 10 ~ 15 個の円が青色になります。
その理由を理解するには、(メイン キューにサブミットしている場合を除いて) 同期作業が呼び出し元スレッドで実行されるという事実を再検討する必要があります。
上記の例では、呼び出し元 (starterQueue) がトップの QoS (userInteractive) を持っています。したがって、一見無害な sync
タスクはスターター キューをブロックしているだけでなく、スターターの高 QoS スレッドでも実行されています。したがって、タスクは高い QoS で実行されますが、その前に background を持つ同じバックグラウンド キューに 2 つの他のタスクがあります。 QoS。優先順位の逆転が検出されました!
予想どおり、GCD はキュー全体の QoS を上げて高 QoS タスクに一時的に一致させることで、この逆転を解決します。その結果、バックグラウンド キューのすべてのタスクは ユーザー インタラクティブ で実行されます。 ユーティリティよりも高い QoS QoS。そのため、ユーティリティ タスクは最後に終了します!
補足:その例からスターター キューを削除し、代わりにメイン キューから送信すると、同様の結果が得られます。これは、メイン キューにも ユーザー インタラクティブ があるためです。 品質。
この例で優先度の逆転を避けるには、sync
でスターター キューをブロックしないようにする必要があります。 声明。 async
の使用 その問題を解決します。
必ずしも理想的ではありませんが、プライベート キューを作成するときやグローバル同時キューにディスパッチするときにデフォルトの QoS に固執することで、優先度の逆転を最小限に抑えることができます。
スレッド爆発
並行キューを使用する場合、注意しないとスレッド爆発のリスクがあります。これは、現在ブロックされている同時実行キューにタスクを送信しようとしたときに発生する可能性があります (たとえば、セマフォ、同期、またはその他の方法で)。あなたのタスクは 実行されますが、システムはこれらの新しいタスクに対応するために新しいスレッドをスピンアップする可能性が高く、スレッドは安価ではありません.
各シリアル キューは 1 つのスレッドしか使用できないため、アプリのサブシステムごとにシリアル キューから始めることを Apple が提案している理由はこれにあると思われます。シリアル キューの関係は同時であることを忘れないでください その他 そのため、作業をキューにオフロードしても、並行していなくてもパフォーマンスが向上します。
レース条件
Swift 配列、辞書、構造体、およびその他の値の型は、デフォルトではスレッドセーフではありません。たとえば、複数のスレッドがアクセスして変更しようとしている場合 同じアレイを使用すると、問題が発生し始めます。
ロックやセマフォを使用するなど、リーダー/ライターの問題にはさまざまな解決策があります。しかし、ここで説明したい関連ソリューションは、分離キューの使用です。
整数の配列があり、この配列を参照する非同期作業を送信したいとしましょう。私たちの仕事が読むだけである限り 配列であり、それを変更しないので安全です。しかし、非同期タスクの 1 つで配列を変更しようとするとすぐに、アプリが不安定になります。
アプリは問題なく 10 回実行でき、11 回目にはクラッシュするため、これは難しい問題です。この状況で非常に便利なツールの 1 つは、Xcode の Thread Sanitizer です。このオプションを有効にすると、アプリで潜在的な競合状態を特定するのに役立ちます。
この問題を示すために、この (確かに不自然な) 例を見てみましょう:
class ViewController: UIViewController {
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
var array = [1,2,3,4,5]
override func viewDidLoad() {
for _ in 0...1 {
race()
}
}
func race() {
concurrent.async {
for i in self.array { // read access
print(i)
}
}
concurrent.async {
for i in 0..<10 {
self.array.append(i) // write access
}
}
}
}
async
の 1 つ タスクは、値を追加して配列を変更しています。これをシミュレーターで実行しようとすると、クラッシュしない可能性があります。しかし、十分な回数実行すると (または 7 行目のループ周波数を増やすと)、最終的にはクラッシュします。スレッド サニタイザーを有効にすると、アプリを実行するたびに警告が表示されます。
この競合状態に対処するために、バリア フラグを使用する分離キューを追加します。このフラグは、キューにある未処理のタスクを終了することを許可しますが、バリア タスクが完了するまで、それ以上のタスクの実行をブロックします。
公衆トイレ (共有リソース) を掃除する用務員のようなバリアを考えてください。人々が使用できるトイレ内には複数の (同時に) 屋台があります。
到着すると、用務員は清掃サイン(バリア)を配置して、清掃が完了するまで新規参入者をブロックしますが、用務員は、内部のすべての人が仕事を終えるまで清掃を開始しません.彼ら全員が去ると、用務員は公衆トイレを隔離して掃除し始めます。
最後に、用務員は看板 (バリア) を取り除き、外で列に並んでいる人が最終的に入ることができるようにします。
コードは次のようになります:
class ViewController: UIViewController {
let concurrent = DispatchQueue(label: "com.besher.concurrent", attributes: .concurrent)
let isolation = DispatchQueue(label: "com.besher.isolation", attributes: .concurrent)
private var _array = [1,2,3,4,5]
var threadSafeArray: [Int] {
get {
return isolation.sync {
_array
}
}
set {
isolation.async(flags: .barrier) {
self._array = newValue
}
}
}
override func viewDidLoad() {
for _ in 0...15 {
race()
}
}
func race() {
concurrent.async {
for i in self.threadSafeArray {
print(i)
}
}
concurrent.async {
for i in 0..<10 {
self.threadSafeArray.append(i)
}
}
}
}
新しい分離キューを追加し、配列を変更するときにバリアを配置する getter と setter を使用してプライベート配列へのアクセスを制限しました。
ゲッターは sync
である必要があります 値を直接返すため。セッターは async
にすることができます 、書き込みが行われている間、呼び出し元をブロックする必要がないため.
バリアなしでシリアル キューを使用して競合状態を解決することもできますが、そうすると、配列への同時読み取りアクセスを持つという利点が失われます。おそらくあなたの場合、それは理にかなっていますが、あなたが決めてください.
結論
ここまで読んでいただきありがとうございました!この記事から何か新しいことを学んでいただければ幸いです。要約といくつかの一般的なアドバイスを残します:
まとめ
- キューは常に開始 FIFO 順のタスク
- キューは other に対して常に並行です キュー
- 同期 vs 非同期 ソースに関する
- シリアル vs 同時 目的地に関する
- 同期は「ブロック」と同義
- 非同期はすぐに呼び出し元に制御を返します
- シリアルは単一のスレッドを使用し、実行順序を保証します
- 同時実行では複数のスレッドが使用され、スレッド爆発のリスクがあります
- 設計サイクルの早い段階で並行性について考える
- 同期コードは推論とデバッグが容易です
- 可能であれば、グローバル同時キューに依存しない
- サブシステムごとのシリアル キューから始めることを検討してください
- 測定可能な場合にのみ同時キューに切り替えます パフォーマンス上の利点
Swift Concurrency Manifesto の「並行性の海にシリアライゼーションの島」があるという比喩が好きです。この感情は、Matt Diephouse によるこのツイートでも共有されました。
並行コードを書く秘訣は、その大部分をシリアルにすることです。同時実行を小さな外側のレイヤーに制限します。 (シリアルコア、コンカレントシェル。)
— Matt Diephouse (@mdiep) 2019 年 12 月 18 日
例えばロックを使用して 5 つのプロパティを管理する代わりに、それらをラップする新しいタイプを作成し、ロック内の単一のプロパティを使用します。
その哲学を念頭に置いて並行性を適用すると、混乱したコールバックで迷子になることなく推論できる並行コードを実現するのに役立つと思います.
ご質問やご意見がございましたら、お気軽に Twitter までお寄せください
ベシェル・アル・マレ
Unsplash の Onur K によるカバー写真
コンパニオン アプリのダウンロードはこちら:
almaleh/DispatcherCompanion アプリを並行性に関する私の記事に追加してください。 GitHub でアカウントを作成して、Almaleh/Dispatcher の開発に貢献してください。 almalehGitHub私の他の記事をチェックしてください:
Fireworks — Swift 用のビジュアル パーティクル エディタパーティクル エフェクトを設計および反復する際に、macOS および iOS 用の Swift コードをオンザフライで生成します ベッシャー アル マレ完璧な iOS [弱い自己] は (常に) 必要ありません。この記事では、弱い自己について説明します。 inside of Swift closures to avoid retain cycles &explore cases where it may or may not be necessary to capture self weakly. Besher Al MalehFlawless iOSFurther reading:
IntroductionExplains how to implement concurrent code paths in an application.Concurrent Programming:APIs and Challenges · objc.ioobjc.io publishes books on advanced techniques and practices for iOS and OS X development Florian Kugler Low-Level Concurrency APIs · objc.ioobjc.io publishes books on advanced techniques and practices for iOS and OS X development Daniel Eggerthttps://khanlou.com/2016/04/the-GCD-handbook/
Concurrent vs serial queues in GCDI’m struggling to fully understand the concurrent and serial queues in GCD. I have some issues and hoping someone can answer me clearly and at the point.I’m reading that serial queues are created... Bogdan AlexandruStack OverflowWWDC Videos:
Modernizing Grand Central Dispatch Usage - WWDC 2017 - Videos - Apple DevelopermacOS 10.13 and iOS 11 have reinvented how Grand Central Dispatch and the Darwin kernel collaborate, enabling your applications to run... Apple Developer Building Responsive and Efficient Apps with GCD - WWDC 2015 - Videos - Apple DeveloperwatchOS and iOS Multitasking place increased demands on your application’s efficiency and responsiveness. With expert guidance from the... Apple Developer-
iOS 11の写真アプリで人を追加または削除する方法
iOS 10 Apple は、顔を認識し、それらを利用して人々の顔を含む写真を整理する革新的な機能を導入しました。そのため、愛する人の写真をすべて見なければならないたびに、すべての写真をめくる代わりに、その人の写真のサムネイルをクリックするだけで済みました。しかし、iOS 11 では多くの変更や修正が行われ、その中にはコンフォート ゾーンから抜け出すようなものもありました。そのようなものの 1 つは、iOS 11 での変更は、人物の写真を含むサムネイルを追加するオプションに直接アクセスできないことです。そこで、iOS 11 を実行している iPhone の写真アプリから人物を追加または削除する
-
iOS 12 でメッセージ内の写真にアクセスする方法
iOS 3 がリリースされて以来、Apple デバイスのメッセージ経由で動画や写真を送信できます。そのためには、カメラ アイコンをタップして、送信する写真またはビデオを選択するだけです。 iOS 12 では、状況が変わりました。ただし、これは完全に無効になっているという意味ではありません。 iOS 12 では、メッセージでカメラ アイコンをタップすると、iPhone のカメラが起動して新しい写真やビデオをキャプチャします。写真をクリックしたり、Apple のすばらしいカメラ効果を使ってビデオを撮影したり、図形やステッカー パックなどを追加したりできます。メッセージのフォト ライブラリにアクセ