すべてのモバイルアプリケーションは、ネットワークからのデータ読み込み、ユーザータッチの処理、インターフェースのアニメーション、ファイル保存など、多くのタスクを同時に実行します。このコードすべてが単一のスレッドで実行されると、ネットワーク遅延が発生するたびにアプリケーションがフリーズします。マルチスレッドと並行処理(concurrency)は、アプリケーションの応答性と効率を維持するための重要な概念です。この記事では、Main ThreadやRunLoopからKotlinのコルーチン、iOSのCombineまで、主要なツールをすべて解説します。内容はApple公式GCDドキュメントに基づいています。
重要なポイント
マルチスレッドとは、アプリケーションが複数のコード断片を同時に実行できる能力です。各断片は個別のスレッド(Thread)で実行されます — 独自のコールスタックを持つ軽量プロセスです。モバイル開発では、スレッドはMain Thread(UIスレッド)とBackground Threads(バックグラウンドスレッド)の2つのカテゴリに分類されます。
オペレーティングシステム自体が、プロセッサコア全体へのスレッドの分散を管理します。最新のデバイスには6〜8個のコアがあるため、並列実行によって作業を高速化できます。ただし、スレッドの作成は高コストな操作であるため、直接Threadを操作することは推奨されません。代わりに、より高レベルの抽象化(DispatchQueue、OperationQueue、CoroutineDispatcher)が使用されます。
並行処理(Concurrency)はマルチスレッドよりも広い概念です。並行処理とは、コンテキストスイッチングによって単一コアでもタスクを「同時に」実行できることを意味します。非同期処理(Async/Await)は、タスクがスレッドをブロックせず、結果を待つ間制御を返すプログラミングモデルです。最新の言語(Kotlin、Swift、Dart)にはAsync/Awaitの組み込みサポートがあります。
IT Sectrでは、プロジェクト開始時に適切なマルチスレッドアーキテクチャに特に注意を払っています。初期段階で犯したミスは、データ競合、デッドロック、負荷時のアプリケーション不安定性など、発見が難しいバグにつながります。当社のプロジェクトはすべて、計画段階で並行処理アーキテクチャのレビューを受けます。
Main Thread(メインスレッド)は、モバイルアプリケーションでUIにアクセスできる唯一のスレッドです。AndroidではUI Thread、iOSではMain Threadと呼ばれます。テキスト変更、アニメーション、タッチ処理など、すべてのインターフェース操作はMain Threadでのみ実行されます。メインスレッドで重い操作(ファイル読み込み、JSON解析)が実行されると、インターフェースの応答が停止します。AndroidではANR(Application Not Responding)が発生し、iOSでは画面が「フリーズ」します。
Background Threads(バックグラウンドスレッド)は、UIに関係しないすべての処理(ネットワークリクエスト、データベース操作、画像処理、暗号化)を対象としています。完了後、結果は表示のためにMain Threadに渡されます。各プラットフォームはスレッド間の切り替えに独自のツールを提供しています:iOSのDispatchQueue.main.async、AndroidのrunOnUiThreadまたはwithContext(Dispatchers.Main)。
RunLoop — iOSのメインスレッドにおけるイベント処理ループです。RunLoopはイベント(タッチ、タイマー、通知)を待機し、適切なハンドラにディスパッチします。Androidでの同等品はLooperで、各Main Threadに関連付けられています。Main Looperはキューからメッセージを無限に取り出し、処理のためにHandlerに渡します。RunLoopとLooperを理解することで、メモリリークやインターフェースの「カクつき」を回避できます。
Grand Central Dispatch(GCD)は、C言語レベルでマルチスレッドを管理するAppleのライブラリです。GCDはDispatchQueue(タスクキュー)を操作します。開発者は手動でスレッドを作成せず、GCDがスレッドプールを管理し、利用可能なプロセッサコアにタスクを分散します。DispatchQueueには2つのタイプがあります:Serial Queue(直列キュー — タスクが順次実行)とConcurrent Queue(並行キュー — タスクが同時に実行可能)。
Main DispatchQueueはメインスレッドにバインドされた直列キューです。Global Queuesは異なる優先度(QoS — Quality of Service)を持つ並行キューです:userInteractive、userInitiated、utility、background。適切なQoSを選択することはパフォーマンスにとって重要です:.userInteractive — UIに影響するタスク(アニメーション、レンダリング)用。.background — 時間に重要でないタスク(同期、キャッシュクリーンアップ)用。
OperationQueueはGCDの上位抽象化で、タスクのキャンセル、操作間の依存関係設定、同時操作の最大数制御などの追加機能を提供します。操作はOperationクラス(またはBlockOperation)のオブジェクトです。例:画像を読み込み、フィルターを適用し、その後で表示する必要がある場合、依存関係のあるOperationQueueが最適です。GCDではDispatchGroupやセマフォを使って手動でこれらのステップを同期する必要があります。
Swift 5.5+のAsync/Awaitは、GCDに代わる最新の代替手段です。asyncキーワードとawaitキーワードにより、非同期コードが直線的で読みやすくなります。関数はasyncとしてマークされ、呼び出しはawaitで待機されます。システム自体がコンテキストスイッチングを管理します:デフォルトではasync関数はバックグラウンドスレッドで実行され、UI更新はMainActorで実行されます。@MainActorはメインスレッドでのコード実行を保証する属性です。
Coroutines(コルーチン)は、JetBrainsによって開発されたKotlin用の軽量スレッドです。通常のスレッドとは異なり、コルーチンは特定のThreadにバインドされません。数千のコルーチンが、大きなオーバーヘッドなしに複数のスレッドで実行できます。CoroutineScopeはコルーチンのライフサイクルを管理します:viewModelScopeはViewModelに、lifecycleScopeはActivity/Fragmentにバインドされます。スコープが破棄されると、すべての子コルーチンが自動的にキャンセルされます。
Dispatchersはコルーチンが実行されるスレッドプールを決定します:Dispatchers.Main — UIスレッド。Dispatchers.IO — ネットワークリクエストとディスク操作用。Dispatchers.Default — CPU負荷の高い計算用。ディスパッチャを切り替えるにはwithContextを使用します。コルーチンは構造化された並行処理(structured concurrency)をサポートしています:各コルーチンには親があり、親がキャンセルされるとすべての子コルーチンがキャンセルされます。これによりメモリリークと中断されたタスクを防止します。
Flow — コルーチンライブラリからのコールド非同期データストリームです。Flowは値を順次発行します:(1)プロデューサーがデータを生成し、(2)オペレーターがストリームを変換し、(3)コレクターが結果を消費します。LiveDataとは異なり、Flowは複雑なオペレーターチェーン(map、filter、flatMapConcat、catch)をサポートし、完全にスレッドセーフです。StateFlowとSharedFlowはFlowのホットバリアントで、UI状態や単発イベント(Snackbar、ナビゲーション)に最適です。
Channel — コルーチン間でデータを渡すためのもう1つのコルーチン抽象化です。Channelはキューとして機能します:1つの送信者(send)と1つ以上の受信者(receive)。バッファリングされたチャネル(Channel(UNLIMITED)、Channel(BUFFERED))はオーバーフロー時の動作を設定できます。ChannelはコールバックベースのAPIをコルーチンに橋渡しするためにFlowと組み合わせてよく使用されます:callbackFlow { … }。
IT Sectrでは、すべてのAndroidプロジェクトでコルーチンとFlowを積極的に使用しています。これにより、同期的に見える非同期コードを記述でき、テストが容易で(runTest、TestDispatcher)、手動のスレッド管理が不要です。データ読み込みを行う簡単なコルーチンの例:
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): List<User> = withContext(Dispatchers.IO) {
return@withContext try {
val users = api.fetchUsers()
dao.insertAll(users)
users
} catch (e: Exception) {
dao.getAll()
}
}
}
リアクティブプログラミング — データが非同期ストリーム(Observable、Publisher)として伝播するパラダイムです。RxJava/RxKotlinはAndroidで最も人気のある実装で、.NET Rxから移植されました。RxSwiftはiOS用の類似ライブラリです。主要コンポーネント:Observable(イベントソース)、Observer(サブスクライバー)、Scheduler(スレッド管理)、Operators(ストリーム変換)。
CombineはiOS 13で導入されたAppleのリアクティブプログラミングフレームワークです。CombineはPublisher(発行者)とSubscriber(購読者)プロトコルを使用します。RxSwiftとは異なり、CombineはSDKに組み込まれており、SwiftUIと緊密に統合されています。Combineのオペレーター:map、filter、combineLatest、zip、debounce、throttle — データバインディングから検索クエリのデバウンスまで、ほとんどのシナリオをカバーします。
FutureとPromise — 単一の非同期結果を扱うためのパターンです。Futureは後で利用可能になる値を表します。Promiseは値を提供する約束です。RxではSingle(1つの成功応答またはエラー)、CombineではFuture Publisherに対応します。実際には、Future/Promiseは単一のAPIリクエストに便利で、Observable/Publisherは継続的なストリーム(位置情報、テキスト入力)に適しています。
CallbackとDelegate — 非同期操作の古典的なパターンです。Callbackは引数として渡され、操作完了時に呼び出される関数です。Delegateはイベントハンドラメソッドを持つプロトコルを実装するオブジェクトです。欠点:「コールバック地獄」(ネストされたコールバック)とエラー処理の複雑さ。NotificationCenter(iOS)とEventBus(Android)はブロードキャストイベントメカニズムで、疎結合な通信に有用ですが、暗黙的な依存関係につながります。
マルチスレッドは高性能への扉を開きますが、同時に発見が難しいエラーのリスクも生み出します。最も一般的なもの:Race Condition(競合状態)、Deadlock(デッドロック)、Livelock(ライブロック)、Starvation(スタベーション)。これらの問題を理解することは、モバイル開発者にとって必須のスキルです。
Race Conditionは、2つ以上のスレッドが同期なしに同じデータを同時に読み書きするときに発生します。結果はどのスレッドが先に実行されるかに依存します。古典的な例:2つのスレッドがカウンターをインクリメントします。「読み取り→インクリメント→書き込み」操作はアトミックではないため、同時に実行されると1つのインクリメントが「失われます」。解決策はアトミック操作(AtomicInteger、AtomicReference)またはロック(Mutex、Semaphore、synchronized)を使用することです。
Deadlock — 各スレッドがリソースを保持し、別のスレッドが保持するリソースを待機する状況です。どのスレッドも進行できません。発生条件:相互排他、保持と待機、横取り不可、循環待機。防止策:ロック取得の単一順序を確立、タイムアウト付きのtryLockを使用、Lock-Freeアルゴリズム(ConcurrentHashMap、CopyOnWriteArrayList)を適用。
Livelock — スレッドはブロックされていませんが、有用な作業を行わずに互いにリソースを「渡し合います」。例:廊下で2人が出会い、両方とも同じ方向に移動しながら道を譲ります。Starvation — 他のスレッドが絶えず横取りするため、あるスレッドがリソースにアクセスできません。解決策:フェアロック(fair locks)、注意してスレッド優先度を使用。
マルチスレッドの問題を防ぐために同期プリミティブが使用されます:Mutex(相互排他)、Semaphore(同時アクセス数の制限)、Lock(tryLock付きインターフェース)、Synchronized(JVMレベルのロック)、@MainActor(Swift — メインスレッドでの実行を保証)。AndroidではExecutors.newFixedThreadPool、newCachedThreadPoolを介してThreadPoolも利用可能です。ただし、手動プール管理はレガシープロジェクトの権限であり、新しいプロジェクトではコルーチンを使用することをお勧めします。
| ツール | プラットフォーム | タイプ | 特徴 |
|---|---|---|---|
| DispatchQueue (GCD) | iOS | タスクキュー | 直列/並行、QoS優先度、スレッドプールはシステム管理 |
| OperationQueue | iOS | 操作キュー | 依存関係、キャンセル、maxConcurrentOperationCount |
| Coroutines + Flow | Android | コルーチン | 軽量、構造化された並行処理、StateFlow、Channel |
| RxJava / RxKotlin | Android | リアクティブストリーム | Observable、Schedulers、豊富なオペレーターセット |
| Combine | iOS | リアクティブストリーム | Publisher/Subscriber、SwiftUI統合 |
| Async/Await + Task | iOS / Android | 非同期モデル | 直線的コード、@MainActor、構造化された並行処理 |
よくある質問
Main Thread(UIスレッド)はインターフェースの描画とタッチ処理を担当します。Background Threadはバックグラウンドタスク(データ読み込み、計算、ネットワーク作業)を実行します。Main Threadのブロックはインターフェースのフリーズを引き起こします(AndroidではANR、iOSではfrozen UI)。
Race Condition — 2つのスレッドが同時に共有データにアクセスし、結果が実行順序に依存する競合状態です。同期によって回避:Mutex、Semaphore、Lock、Synchronized、@MainActor、またはアトミック操作。
CoroutinesはAndroidの最新標準です(JetBrains、Googleサポート)。RxJava/RxKotlinは豊富なオペレーターセットを備えたリアクティブアプローチです。Coroutinesは非同期呼び出しにシンプル、RxJavaは複雑なデータストリームに強力です。IT Sectrでは新規プロジェクトにCoroutines + Flowを使用しています。
Deadlock — 2つのスレッドが互いのリソースを待機する相互ブロックです。Livelock — スレッドはブロックされていませんが、有用な作業なしにリソースを渡し合います。両方の問題は適切なロック順序とタイムアウトで解決されます。
DispatchQueueはスレッド管理のためのGrand Central Dispatch(GCD)の抽象化です。Main Queueはメインスレッドでタスクを実行し、Global Queuesはバックグラウンドスレッドで実行します。Serial Queueは順次実行を保証し、Concurrent Queueは並列実行を保証します。最近のプロジェクトではGCDはAsync/AwaitとTaskに置き換えられることがよくあります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。