Thread — プロセッサ時間の基本単位であり、独自のスタックを持ち、他のスレッドから独立して実行されます。モバイル開発では、長時間の処理中もインターフェースの応答性を保つために、スレッドが並列実行に使用されます。Androidはjava.lang.Thread、Executors、Kotlin Coroutinesをサポートし、iOSはThread(Objective-C)、GCD、OperationQueueをサポートします。Android Thread Documentationによると、ネイティブスレッドの作成にはオペレーティングシステムによるスタック用の約1MBの割り当てが必要です。
重要なポイント
Thread(実行スレッド) — オペレーティングシステムがCPUコアにスケジュールできる命令の独立したシーケンスです。各プロセス(アプリケーション)には少なくとも1つのスレッド(メインスレッド)が含まれます。追加のスレッドはタスクの並列実行のために作成されます。各スレッドは独自のプログラムスタック(ローカル変数を含む)、プログラムカウンタ(PC)、レジスタを持ちます。ヒープメモリはプロセスのすべてのスレッドで共有されます。
モバイルOSでは、スレッドはプリエンプティブマルチタスクによってスケジュールされます:OSはいつでもスレッドの実行を中断し、別のスレッドに制御を渡すことができます(コンテキストスイッチ)。コンテキストスイッチはコストの高い操作です(1〜10マイクロ秒)。CPUレジスタの保存/復元、TLBの更新、キャッシュのフラッシュが必要だからです。これが、過剰な数のスレッド(数百から数千)がパフォーマンスを低下させる理由です — OSは実行よりも切り替えに多くの時間を費やします。
スレッドとプロセス — 異なる概念です。プロセスは専用の仮想メモリを持つアプリケーションのインスタンスです。プロセス内のスレッドはこのメモリを他のスレッドと共有します。Androidでは、アプリケーションの各コンポーネント(Activity、Service、BroadcastReceiver)は1つのプロセスで動作しますが、異なるスレッドで実行できます。iOSアプリケーションも、GCDやThreadを介して追加スレッドを作成できる単一のプロセスです。
各スレッドはJava/Kotlin(Android)とNSThread(iOS)で5つの状態を経ます:New(作成)、Runnable(実行準備完了)、Running(CPU上で実行中)、Blocked/Waiting(リソースまたは通知待ち)、Terminated(終了)。状態間の遷移はOSスケジューラと同期プリミティブによって管理されます。開発者はスレッドの優先度(Thread.setPriority())とその状態(sleep、join、interrupt)に影響を与えることができます。
Androidでは、スレッドはビジーモニター(synchronized)の取得を試みたり、Object.wait()やThread.sleep()を呼び出したりするとBlocked状態になります。iOSでは — NSCondition.wait()、pthread_cond_wait()、またはdispatch_semaphore_wait()を呼び出した場合です。Blocked状態ではスレッドはCPUを消費しませんが、メモリ(スタック)を占有します。スレッドは別のスレッドから割り込み(interrupted)を受け、InterruptedException(Java)またはisCancelled(Kotlin Coroutines)を受け取ることがあります。
| 状態 | 説明 | 遷移メソッド |
|---|---|---|
| New | スレッド作成済み、未起動 | Thread()コンストラクタ |
| Runnable | 実行準備完了、CPU待ち | thread.start() |
| Running | CPUコア上で実行中 | OSスケジューラ |
| Blocked/Waiting | リソース、モニター、通知を待機中 | synchronized、wait()、sleep() |
| Terminated | run()完了または割り込みにより終了 | run()完了、interrupt() |
Context switch(コンテキストスイッチ) — OSが現在のスレッドの状態(レジスタ、PC、TLB)を保存し、別のスレッドの保存された状態をロードする操作です。モバイルシステム(Linux + ART、iOSのXNU)では、コンテキストスイッチに1〜10マイクロ秒かかります。スレッドが100マイクロ秒でタスクを実行し、コンテキストスイッチに5マイクロ秒かかる場合、5%の時間が無駄になります。コンテキストスイッチを最小限に抑えるため、iOSはwork stealingを備えたGCDを使用し、AndroidはfixedThreadCountのプールを使用します。
Androidは低レベルのjava.lang.Threadから現代のコルーチンへと進化しました。抽象化の各レベルは、より少ないオーバーヘッドでより多くの機能を提供します。Threadは基本クラスですが、直接作成は推奨されません:新しいスレッドはプールで管理されず、監視やキャンセルが困難です。AsyncTask(API 30で非推奨)は一歩前進でしたが、メモリリークや設定処理の不便さに悩まされていました。
HandlerThread — Looperを持つThreadの特別なサブクラスで、メッセージキューを処理できます。これはバックグラウンドスレッドでの逐次的なタスク実行に使用され、例えばRoomやファイルへのデータ書き込みなどに使われます。HandlerThreadはstart()を呼び出して作成され、その後Handler(handlerThread.looper)を介してメッセージやRunnableを送信できます。handlerThread.quit()を呼び出すとLooperが停止し、スレッドが終了します。
// Android:Thread、HandlerThread、Executors
import android.os.Handler
import android.os.HandlerThread
import java.util.concurrent.Executors
class ThreadExample {
// 1. Threadの直接作成(非推奨)
fun directThread() {
val thread = Thread(Runnable {
Thread.sleep(1000)
print("Direct thread executed")
})
thread.start()
}
// 2. HandlerThread — 逐次バックグラウンドタスク用
fun handlerThreadExample() {
val handlerThread = HandlerThread("BackgroundQueue")
handlerThread.start()
val handler = Handler(handlerThread.looper)
handler.post {
// バックグラウンドスレッドでの逐次実行
Thread.sleep(500)
print("HandlerThread:タスク完了")
}
// スレッドの停止(タスク完了時に実行)
handlerThread.quitSafely()
}
// 3. Executors — スレッドプール
fun executorExample() {
val executor = Executors.newFixedThreadPool(4)
for (i in 1..10) {
executor.execute {
print("Task $i on thread ${Thread.currentThread().getName()}")
}
}
executor.shutdown()
}
// 4. Kotlin Coroutines — 現代の標準
suspend fun coroutineExample() = kotlinx.coroutines.withContext(
kotlinx.coroutines.Dispatchers.Default
) {
print("Coroutine on thread: ${Thread.currentThread().getName()}")
}
}
例ThreadExampleはAndroidでの4つのスレッド抽象化レベルすべてを示しています。Threadの直接作成は、最も低レベルで非効率的なアプローチです。HandlerThreadはバックグラウンドでの逐次タスクに便利です。Executors.newFixedThreadPool(4)は最大10タスクの並列実行のために4スレッドのプールを作成します。Dispatchers.Defaultを使用するKotlin Coroutinesは、現代的で効率的かつ安全な方法です。
HandlerThread — 組み込みのLooperとメッセージキューを持つThreadの特殊化されたサブクラスです。start()を呼び出して作成され、その後Handler(handlerThread.looper)を介してRunnableやメッセージを送信できます。HandlerThreadはタスクを厳密に逐次的に実行します — 前のタスクが完了するまで次のタスクは開始されません。これは操作の順序が重要なRoomやファイルへのデータ書き込みに便利です。quitSafely()を呼び出すと、現在のタスク完了後にLooperが停止します。
iOSもスレッド操作の3つのレベルを提供します。Thread(SwiftのThread、Objective-CのNSThread) — ネイティブスレッドを直接作成する低レベルAPI。DispatchQueueを介したGCD(Grand Central Dispatch) — iOS開発者のための主要ツールで、スレッドプールを自動管理します。OperationQueue — 依存関係、優先度、キャンセルをサポートするGCD上の高レベル抽象化です。
現代のiOS開発でのThreadの直接使用は非常に稀です — GCDは自動メモリ管理とスレッド管理で必要な機能をすべて提供します。Threadは特定のケースでのみ使用されます:スレッドローカルストレージ(threadDictionary)の設定、バックグラウンドスレッド用のRunLoopの作成、pthread_tを期待するCライブラリとの統合などです。
import Foundation
class ThreadManager {
// 1. Thread(低レベル)
func createThread() {
let thread = Thread {
// コードが新しいスレッドで実行される
print("Current thread: \(Thread.current)")
}
thread.name = "com.app.worker"
thread.qualityOfService = .utility
thread.start()
}
// 2. GCD — DispatchQueue
func gcdExample() {
// 並列キュー
let queue = DispatchQueue(label: "com.app.concurrent",
qos: .utility,
attributes: .concurrent)
queue.async {
print("GCD async task")
}
// 書き込み同期のためのバリア
queue.async(flags: .barrier) {
// 書き込み中の排他アクセス
print("Barrier write: exclusive access")
}
}
// 3. 依存関係付きOperationQueue
func operationQueueExample() {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .background
let download = BlockOperation {
print("Downloading...")
}
let process = BlockOperation {
print("Processing...")
}
let save = BlockOperation {
print("Saving...")
}
// 依存関係:download -> process -> save
process.addDependency(download)
save.addDependency(process)
queue.addOperations([download, process, save], waitUntilFinished: false)
}
}
// GCDバリアによるスレッドセーフコレクション
class ThreadSafeArray<T> {
private var array: [T] = []
private let queue = DispatchQueue(label: "com.app.concurrent",
attributes: .concurrent)
var count: Int {
return queue.sync { array.count } // concurrent read
}
func append(_ element: T) {
queue.async(flags: .barrier) { // exclusive write
self.array.append(element)
}
}
}
クラスThreadSafeArrayはGCDバリアを介したConcurrent Read / Exclusive Writeパターンを示しています。queue.sync{}による読み取りは複数のスレッドから並列に実行されます。queue.async(flags: .barrier)による書き込みは、書き込みが完了するまで他のすべての操作(読み取りと書き込みの両方)をブロックします。これはsynchronizedブロックよりも効率的です。書き込みがない限り読み取りをブロックしないからです。
iOSでThreadを直接使用するのは3つの場合に正当化されます:スレッドローカルストレージ(Thread.current.threadDictionary) — スレッドにバインドされたデータの保存;バックグラウンドスレッドでperformSelector:onThread:を使用した特別なRunLoopの作成;pthread_tを期待するC/C++ライブラリとの統合。その他のすべての場合は、DispatchQueueを介したGCDが推奨されます — 自動的にスレッドプールと電力消費を管理します。
Race condition(競合状態)は、2つ以上のスレッドが同時に共有データにアクセスし、少なくとも1つのスレッドが書き込みを行うときに発生します。結果は実行の順序(タイミング)に依存し、予測不能です。競合状態を防ぐために同期プリミティブが使用されます。モバイル開発では、ロック(synchronized、NSLock)、アトミック操作(AtomicInteger、iOSのatomicプロパティ)、キュー(シリアルキュー)が利用可能です。
プリミティブの選択はシナリオに依存します。単純なカウンターやフラグにはアトミック操作(AtomicInteger、atomicプロパティ)で十分です。複数の操作を含むクリティカルセクションには — ロック(synchronized、NSLock)。複雑なデータ構造には — シリアルDispatchQueueまたはGCDバリア。ロックは理解しやすいですが、デッドロックやライブロックの影響を受けやすいです。キューはより複雑ですが、より安全です。
// Android/Kotlinでの同期
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class Counter {
// 1. AtomicInteger — 単純なカウンター用
private val atomicCount = AtomicInteger(0)
fun incrementAtomic() = atomicCount.incrementAndGet()
// 2. synchronized — クリティカルセクション用
@Synchronized
fun synchronizedOperation() {
// 一度に1スレッドのみ
doWork()
}
// 3. コルーチンのMutex — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// 保護されたコード — スレッドセーフ
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// デッドロックの例:AがBをロック、BがAをロック
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun methodA() = synchronized(lockA) {
Thread.sleep(100)
synchronized(lockB) { print("OK") }
}
fun methodB() = synchronized(lockB) {
Thread.sleep(100)
synchronized(lockA) { print("OK") }
}
}
Counterは同期の3つのアプローチを示しています。AtomicInteger.incrementAndGet() — ロックなしのアトミック操作(CAS)。@Synchronized — Javaの組み込みモニターで、オブジェクト全体をロックします。Mutex.withLock — コルーチンミューテックスで、スレッドをブロックする代わりにコルーチンを一時停止します(より効率的)。DeadlockExampleは古典的なデッドロックを示しています:2つのスレッドが異なる順序でロックを取得します。
Thread Pool(スレッドプール) — タスク実行のために再利用される事前作成されたスレッドのセットです。各タスクに対して新しいスレッドを作成する(高コスト)代わりに、プールはプールから空きスレッドを取得します。空きスレッドがない場合、タスクはキューに入れられます。プールは自動的にサイズを管理します:ピーク負荷時には新しいスレッドが作成され、アイドル状態のスレッドは終了されます。これによりスレッド作成のオーバーヘッドが数十倍削減されます。
AndroidではExecutors.newFixedThreadPool(4)が4スレッドのプールを作成します。同時に10のタスクが来た場合、4つが即座に実行を開始し、6つがキューで待機します。Executors.newCachedThreadPool()は必要に応じてスレッドを作成し(上限なし)、60秒後にアイドルスレッドを終了します。iOSではGCDが自動的にグローバルキューのプールを提供し、そのサイズはCPUコア数と現在の負荷に応じて調整されます。
Kotlin Coroutinesでは、スレッドプールはディスパッチャー内部に隠されています。Dispatchers.DefaultはCPUコア数と等しいサイズのプールを使用します(最小2)。Dispatchers.IO — 64スレッド(数百のIOバウンドタスクに十分で、ほとんどが入出力を待機しCPUを占有しません)。各ディスパッチャーは負荷に応じて自動的にプールをスケールし、アイドル時にはバッテリーを節約します。
よくある質問
Thread — アプリケーションにおけるコード実行の基本単位です。各プロセスは複数のスレッドを持つことができ、メモリを共有しますが、独自のスタックを持ちます。モバイル開発では、UIをブロックせずにタスクを並列実行するためにスレッドが使用されます。AndroidはThread、Executors、HandlerThread、Coroutinesを使用します。iOSはThread、GCD(DispatchQueue)、OperationQueueを使用します。
Threadの作成にはAndroidで約1MB、iOSで約512KBのスタック割り当てが必要で、これは高コストな操作です。1000タスクに対して1000スレッドを直接作成すると、スタックだけで約1GB必要になり、さらにコンテキストスイッチのオーバーヘッドが加わります。Threadの代わりにプール(Executors、GCD)やコルーチンを使用してください — スレッドを再利用し、オーバーヘッドを数十倍削減します。
Race condition — 複数のスレッドが同時に共有データに書き込みアクセスする際の予測不能な動作です。3つの方法で回避できます:アトミック型(AtomicInteger)の使用、ロック(synchronized、NSLock)の使用、キュー(シリアルDispatchQueue、KotlinのActor)によるアクセスの直列化。ベストプラクティスは共有mutable状態を最小限にし、イミュータビリティを使用することです。
Thread — 約1MBのスタックを占有し、OSコアにバインドされたネイティブシステムオブジェクトです。コルーチン — 特定のスレッドにバインドされず、ブロッキングなしで一時停止(suspend)できるKotlinの軽量実行単位です。1つのスレッドで数千のコルーチンを実行できます。コルーチンはメモリ効率が良く、コールバックなしで非同期コードを記述できます。
DeadlockはANRなしのアプリケーション完全フリーズとして現れます。AndroidではThread.getAllStackTraces()を使用して全スレッドのスタックダンプを取得します — 2つのスレッドが互いのロックを待っていることがわかります。iOSでは — Thread.callStackSymbols。ツール:Android Studio Profiler(Threadsタブ)、Instruments(iOS、Thread State View)。防止策:ロックを固定順序で取得し、タイムアウト付きtryLockを使用します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。