🤖

【翻訳】Introduction to Concurrency in iOS using Grand Central Dispatch

2023/07/17に公開

複数のコードを同時に実行するには?
コーディングを始めるとき、私たちは直列に実行するコードや、順番に1つずつ実行するコードを教わります。

例を見てみましょう。簡単なサラダを作るとます。ここに材料があります。

レタス
トマト
赤玉ねぎ
スイートコーン
ツナのひまわり油和え
作り方は簡単で、切る必要があるものを切り、全部混ぜます。

addLettuce()
addTomatoes()
addRedOnion()
addSweetcorn()
addTuna()
mix()

このシンプルなサラダは、材料を入れる順番が決まっていません。
もし5人の人間がいれば、それぞれが1つの仕事をこなし、
5つの仕事は同時に(同時進行で)こなすことができます。

各タスクが単独で1分かかるなら、サラダを作るのに5分かかります。
しかし、すべてのタスクが同時に行われた場合、サラダを作るのに1分しかかかりません。

だから、同時並行的に仕事を進める方法を学ぶことが、時間を節約するために重要なのは明らかです。
しかし、iOSでタスクを並行して実行する方法を学ぶべき理由として、
時間の節約以上に重要な理由がもう1つあります。

この記事では、iOS開発において並行処理を学ぶことがなぜ重要なのかを説明します。
次に、iOS上でコードを並行して実行する方法の例を見ていきます。
最後に、サラダを作るアプリでの並行処理を探ります。

この記事では、あなたがすでにiOSアプリ開発とプログラミング言語Swiftの基本に精通していると仮定します。

この投稿ではXcode 12.2を使用しました。

なぜiOSで並行処理を学ぶのか?

iOSでは、アプリのユーザーインターフェース(ユーザーが操作するビュー)は
常にレスポンシブでなければなりません。つまり、ユーザーが常に操作できなければなりません。
ユーザーが画面上のボタンをタップしても何も起こらないと、ユーザーはアプリがフリーズしていると判断します。これは本当に悪いユーザー体験です。さらに、iOSのオペレーティング・システムは、一定期間反応がないと
アプリを終了させます。しかし、応答性と並行処理に何の関係があるのでしょうか?

サラダの例に戻りましょう。あなたがサラダレストランを経営していて、従業員がいないとしましょう。
あなたは客が来るまでレジで待っています。客がサラダを注文し、あなたはサラダを作ります。

あなたのレストランを訪れた客はあなたと接します。あなたはレストランにおけるカスタマー・インターフェイス
(ソフトウェアでいうユーザー・インターフェイス)なのです。そうやって顧客はあなたのビジネスに接します。
彼らはあなたに話しかけて、注文をします。

注文を受けると、あなたは戻ってこの注文を作り始めます。サラダを作っている間に二人目の客が来ます。
しかし、レジに誰もいないのを確認し、誰かいないか尋ねたが反応がなかったので、
その客は帰ってしまいました。このビジネスは無反応です。
潜在的な顧客は嫌な顧客体験をし、二度と来ないと決めました。これではビジネスは成り立たなくなります。

並行処理を学ぶことは、自分のレストランで必要なときに必要な仕事をするために、
他の人(労働者)を雇えるようになるようなものです。あなたはレジを離れる必要はありません。
あなたはいつでもお客に対応できます。

iOSでタスクを同時に実行するには?

iOSでは、Dispatch(Grand Central Dispatchとも呼ばれ、
頭文字をとってGCDとも呼ばれる)と呼ばれるフレームワークを使って、
複数のタスクを同時に実行することができます。

GCDは、労働者(より具体的にはキューと呼ばれる)に特定のタスクを実行させるためのツールを
提供してくれます。ワーカーを管理するためにDispatchフレームワークが提供する方法はたくさんあります。
この記事では、最もよく使われる2つの方法のみを取り上げます。

労働者のインスタンスを作成し、タスクを実行するように指示します。
GCDに実行するタスクを伝え、どの労働者に割り当てるかを決定させます。

1.ワーカーのインスタンスを作成し、タスクの実行を指示する

労働者はGCD用語でシリアルキューと呼ばれます。これらはタスクを先入れ先出し(FIFO)で実行します。
労働者またはシリアルキューは、手元のタスクが実行されるまで、次のタスクを実行しません。

労働者を作る

let worker1 = DispatchQueue(label: "worker1")

これで労働者にタスクを実行させることができます。しかし、労働者に仕事を指示して、
私たちは自分の道を行くこともできますし(非同期)、労働者が仕事を終えるのを待つこともできます(同期)。
労働者に指示を出すには

func performTaskAsynchronous() {
    worker1.async { // 1
        // do something that takes 2 seconds     
        print("this will print after") // 3
    }
    print("this will print before") // 2
}
func performTaskSynchronous() {
    worker1.sync { // 1
        // do something that takes 2 seconds     
        print("this will print before") // 2
    }
    print("this will print after") // 3
}

上の例では、performTaskSynchronous は、worker1.sync が終了した後にのみステートメントを
実行することに注意してください。しかし、performTaskAsynchronous は worker1.async を実行し、worker1 のタスク終了を待たずにすぐに次のステートメントを実行します。

この記事では主に async を利用します。

2.GCDにタスクを伝え、どの作業者に割り当てるかを決定させる

各食材に労働者を雇っているとしましょう。もしあなたが自分の労働者を管理しているのであれば、
各成分ごとに新しい労働者を追加する必要があります。それぞれの労働者は1つの食材に関連付けられます。
2つの仕事をこなす労働者はいません。新しい労働者を作って、1回の呼び出しでタスクを実行するように
指示できたら便利だと思いませんか?Dispatchフレームワークは、globalという名前の関数を使って、
すでにそれを実現しています。その仕組みを見てみましょう。

DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated).async { 
    // do something
}

関数にQoS値を与えなければならないことに注意しましょう。
これはフレームワークにコードの実行の優先順位を指示します。優先順位についてはあまり深く掘り下げません。
しかし、コードの実行がユーザーにとって現時点でどれだけ重要かを考えてみてください。
この場合、ユーザーはサラダがユーザーのリクエストによって作られるのを待っています。
したがって、.userInitiated優先順位を提供しました。

Grand Central Dispatchを使ってタスクを並行処理する方法

このセクションでは、DispatchフレームワークのDispatchQueueを実際に使ってみます。
まずメインスレッドですべての処理を行い、次に労働者のシリアルキューを1つ経由し、
最後にグローバルコンビニエンス関数を使用してすべての材料を並行して準備することで、
ソフトウェアで簡単なサラダを作る方法を探ります。
すでにあるアプリから始めます。このアプリには、それぞれの材料を準備するクラスが含まれています。
材料を混ぜる準備ができたら、それをサラダのボウルに加えます。材料が混ざるごとにUIを更新し、
ユーザーがサラダの進行状況を確認できるようにします。

ステップ

スターターパックをダウンロードする
一人の作業員でサラダを作る
各材料を一人一人の作業員で準備する

さあ、始めましょう。

1.スターターパックのダウンロード

まずはスターターパックをダウンロードしましょう。ターミナルアプリを開き、以下のコマンドを実行します。

cd $HOME
curl https://github.com/anuragajwani/SaladMaker/archive/starter.zip -o salad_maker.zip -L -s
unzip -q salad_maker.zip
cd SaladMaker-starter
open -a Xcode SaladMaker.xcodeproj

この投稿では、主にSaladMaker.swiftファイルのSaladMakerクラスに焦点を当てます。

2.一人の労働者でサラダを作る

XcodeのSaladMaker.swiftでSaladMakerを開きます。
現在、このファイルはサラダの材料を次々に作っています。
スリープを使うことで、下ごしらえの時間を1秒遅らせています。
スリープ時間を長くすると、iOSがアプリを殺す可能性が高くなります。アプリを実行します。

このアプリでは、すべての食材が準備中または準備完了であることを同時に表示しています。コードを見ると、
各材料が準備できた後にUIを更新しています。しかし、UIはすべての食材が準備できるまで更新されません。
なぜか?UIを更新するはずの労働者が次の食材の準備で忙しいからです。

サラダの下ごしらえを始める前に1.5秒のディレイを追加しました。
しかし、1.5秒後に「キャンセル」ボタンをタップしても、画面がすぐに変わらないことに注意してください。
UIが更新されるまで長い時間がかかることがあります。なぜでしょう?
この場合も、労働者はまずすべての食材を下ごしらえし、次にUIを更新し、
サラダの下ごしらえ中に指示された新しいコマンドを実行するのに忙しいのです。

サラダの下ごしらえをする労働者を雇って、この問題を解決しましょう。
SaladMakerを開き、make関数の実装を以下のように変更します。

func make(onIngredientPrepped: @escaping (Ingredient) -> ()) {
    let worker1 = DispatchQueue(label: "salad_maker_queue")
    Ingredient.allCases.forEach { (ingredient) in
        worker1.async {
            let randomPrepTime = UInt32.random(in: 1...3)
            sleep(randomPrepTime)
            DispatchQueue.main.async {
                onIngredientPrepped(ingredient)
            }
        }
    }
}

上記では、各材料を準備する労働者を用意しました。
さらに、各食材を仕上げた後、労働者は完了をUIスレッドに通知します。

アプリを実行すると、すべての食材が次々と調理され、UIが迅速に更新されていることに気づきます。

3.サラダの下ごしらえを別の作業員に任せる

前のステップでは、各材料を準備するために1人の労働者を採用しました。食材の準備は順番に行われました。最初にレタス、次に玉ねぎといった具合に。しかし、このサラダには準備の順番がありません。もし、
各材料の下ごしらえに1人の作業員を使うことができれば、下ごしらえにかかる時間を最大80%節約できます!

食材1つにつき作業員を1人雇うことができます。あるいは、Dispatchフレームワークが提供する便利な機能を使って、労働者に仕事を割り振ることもできます。後者をやってみましょう。
SaladMakerを開き、make関数を以下のように変更します。

func make(onIngredientPrepped: @escaping (Ingredient) -> ()) {
     Ingredient.allCases.forEach { (ingredient) in
         DispatchQueue.global(qos: .userInitiated).async {
              let randomPrepTime = UInt32.random(in: 1...3)
              sleep(randomPrepTime)
              DispatchQueue.main.async {
                  onIngredientPrepped(ingredient)
              }
         }
    }
}

上のコードでは、労働者やキューの作成を削除しました。
そして、worker1.async { ... を変更しました。}for DispatchQueue.global(qos: .userInitiated).async { ...}.単純に、Dispatchフレームワークに仕事を割り当てるように指示しているだけです。Dispatchフレームワークは次に、システムリソースに基づいて労働者の数を決定します。
ほとんどの場合、各成分を処理するために5人ずつの労働者を雇うでしょう。

アプリを実行し、これまで以上に速く完了するのをご覧ください!

まとめ

この投稿で我々は以下の事を学びました。

並行処理とは何か
iOSで並行処理を学ぶ理由
iOSで並行タスクを実行する方法

最終メモ

全ソースコードはGithubにあります。
https://github.com/anuragajwani/SaladMaker

同時並行性をマスターするには時間がかかるので、気楽に構えてください。

この投稿で使用した例では、準備に特定の順序を必要としないサラダを作ることを探求しました。
しかし、ボウルの中の各材料を追跡した後、サラダの完成状況をどのように管理したか。
別の食材を追加する場合は、その食材のステータスも考慮しなければなりません。
しかし、一連の非同期タスクの完了を管理する、よりエレガントでスケーラブルな方法はあるのでしょうか?「DispatchGroupを使って並列非同期処理を実行する方法」という投稿でそのトピックを取り上げました。

【翻訳元の記事】

Introduction to Concurrency in iOS using Grand Central Dispatch (GCD)
https://anuragajwani.medium.com/introduction-to-concurrency-in-ios-using-grand-central-dispatch-gcd-8280b57a91ec

Discussion