iOSの非同期処理をメインスレッドとバックグラウンドスレッドから理解する

はじめに

こんにちは!アプリ開発支援部の小林です!

iOSのアプリを開発していると、多くの場面で非同期処理が登場します。
しかし、「非同期処理とは何か?」と改めて聞かれると、説明するのは難しいのではないでしょうか?(私もです)

なんとなく、「時間のかかる処理には async/awaitTask を使う」ということは知っていても、実際にその仕組みを理解するのは容易ではありません。

そこで、本記事では、非同期処理がある場合と無い場合を比較しつつ、メインスレッドとバックグラウンドスレッドの観点からiOSの非同期処理についての解説を行います。
そして、非同期処理を「使える」から「理解できる」へステップアップすることを目指します。

非同期処理が無いとどうなるか

非同期処理がある場合と無い場合とでアプリの挙動にどのような違いが生じるのでしょうか?
非同期処理について学ぶ前にサンプルコードを使って非同期処理の有無に伴うアプリの挙動について比較してみましょう。

非同期処理がある場合

以下は、非同期処理のサンプルコードです。
ボタンをタップしたら3秒待機の処理が実行され、その間にローディングのProgressViewが表示されるというシンプルなアプリです。

import SwiftUI
 
struct ContentView: View {
    @State private var isLoading = false
    
    var body: some View {
        VStack(spacing: 40) {
            if isLoading {
                ProgressView("処理中...")
                    .progressViewStyle(CircularProgressViewStyle())
                    .scaleEffect(2)
            }
            
            Button("重い処理開始") {
                Task {
                    await doHeavyWork()
                }
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
        }
        .padding()
    }
    
    // 3秒待機する(重い処理を擬似的に再現)
    func doHeavyWork() async {
        isLoading = true
        
        // 何もしないで3秒待つ
        try? await Task.sleep(nanoseconds: 3_000_000_000)
        
        isLoading = false
        print("処理終了")
    }
}

サンプルコードを実行してみましょう。

ボタンをタップすると、3秒間ProgressViewが表示されています。

非同期処理が無い場合

以下は、非同期処理がない(=同期処理)場合のサンプルコードです。
内容としては、先ほどのサンプルコードの非同期処理の部分を同期処理に変えたコードになります。

import SwiftUI
 
struct ContentView: View {
    @State private var isLoading = false
    
    var body: some View {
        VStack(spacing: 40) {
            if isLoading {
                ProgressView("処理中...")
                    .progressViewStyle(CircularProgressViewStyle())
                    .scaleEffect(2)
            }
            
            Button("重い処理開始") {
                doHeavyWork()
            }
            .padding()
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
        }
        .padding()
    }
    
    // 3秒待機する(重い処理を擬似的に再現)
    func doHeavyWork() {
        isLoading = true
        
        let start = Date()
        while Date().timeIntervalSince(start) < 3 {
            // 何もしないで3秒待つ
        }
        
        isLoading = false
        print("処理終了")
    }
}

こちらもサンプルコードを実行してみましょう。

3秒待機の処理を実行しているにもかかわらず、先ほどの非同期処理のサンプルコードとは異なり、ローディング中にProgressViewが表示されなくなってしまいました。
また、ローディング中にボタンがタップできなくなってしまいました。

では、なぜ同期処理と非同期処理でアプリの挙動にこのような違いが生じたのでしょうか?

メインスレッドとバックグラウンドスレッド

まず、「メインスレッド」と「バックグラウンドスレッド」について押さえておきましょう。
この2つが同期処理と非同期処理でアプリの挙動が異なる原因を理解するカギとなります。

そもそも、「スレッド」とは、簡単に言ってしまうとプログラムの処理が実行される作業レーンのようなものです。
作業レーンなので、1つのレーンを使って1つずつ順番に作業を行う「シングルスレッド」もあれば、複数レーンを使って同時並行作業を行う「マルチスレッド」もあります。

「メインスレッド」とは、iOSアプリにおいてUIの処理を担当しているスレッドになります。
例えば、ボタン入力の受付やローディングのアニメーション描画もこのメインスレッドが担っています。
UIKitやSwiftUIでのUI操作は必ずメインスレッドで行う必要があり、メインスレッドがブロックされるとUI更新が止まってしまいます。

「バックグラウンドスレッド」とは、メインスレッド以外のスレッドになります。
メインスレッドとは独立しているスレッドなので、バックグラウンドスレッドで重い処理を実行してもメインスレッドはその影響を受けません。

非同期処理

メインスレッドとバックグラウンドスレッドを理解すると、先ほどのサンプルコードにおいて同期処理と非同期処理でアプリの挙動が異なった理由が見えてくると思います。
同期処理と非同期処理でアプリの挙動が異なったのは、3秒待機を行う関数doHeavyWorkがメインスレッドで実行され、メインスレッドが3秒間占有されてしまったためです。
同期処理のサンプルコードでは、UIを担うメインスレッドが3秒間ブロックされたことにより、再描画やイベント処理が止まってしまい、ローディング中にProgressViewが表示されなかったり、ボタンがタップできなくなったりしてしまったのです。

ここで活躍するのが「非同期処理」になります。
非同期処理では、メインスレッドにてUIの処理を行いつつ、時間のかかる処理(API通信やファイル読み書き等)をバックグラウンドスレッドに委譲することができます。
これより、メインスレッドが占有されるのを防ぐことができるため、アプリのUIに影響を与えることなく時間のかかる処理を実行することができるのです。

まとめ

本記事では、iOSにおける非同期処理について、同期処理と非同期処理を比較し、メインスレッドとバックグラウンドスレッドの観点から解説を行いました。
メインスレッドとバックグラウンドスレッドの役割を知っておくことにより、非同期処理への理解をより深めることができると思います。