モバイル開発におけるThread — その概要、種類、スレッド管理

著者: IT Sectr 公開日: 2026-03-16 読了時間: 11 分

Thread — プロセッサ時間の基本単位であり、独自のスタックを持ち、他のスレッドから独立して実行されます。モバイル開発では、長時間の処理中もインターフェースの応答性を保つために、スレッドが並列実行に使用されます。Androidはjava.lang.Thread、Executors、Kotlin Coroutinesをサポートし、iOSはThread(Objective-C)、GCD、OperationQueueをサポートします。Android Thread Documentationによると、ネイティブスレッドの作成にはオペレーティングシステムによるスタック用の約1MBの割り当てが必要です。

重要なポイント

  • Thread — CPUスケジューリングの最小単位:各スレッドは独立しており、独自のスタックを持つ
  • スレッド作成にはAndroidで約1MB、iOSで512KBのスタックが必要。そのためプールの方が直接作成より効率的
  • Android:Thread、Executors、HandlerThread、Coroutines — 4つのスレッド抽象化レベル
  • iOS:Thread(低レベル)、GCD(DispatchQueue)、OperationQueue(高レベル)
  • Thread safety — mutableデータへの共有アクセスには同期化が必要:ロック、atomic、シリアルキュー

Threadとは

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()
RunningCPUコア上で実行中OSスケジューラ
Blocked/Waitingリソース、モニター、通知を待機中synchronized、wait()、sleep()
Terminatedrun()完了または割り込みにより終了run()完了、interrupt()

コンテキストスイッチとそのコスト

Context switch(コンテキストスイッチ) — OSが現在のスレッドの状態(レジスタ、PC、TLB)を保存し、別のスレッドの保存された状態をロードする操作です。モバイルシステム(Linux + ART、iOSのXNU)では、コンテキストスイッチに1〜10マイクロ秒かかります。スレッドが100マイクロ秒でタスクを実行し、コンテキストスイッチに5マイクロ秒かかる場合、5%の時間が無駄になります。コンテキストスイッチを最小限に抑えるため、iOSはwork stealingを備えたGCDを使用し、AndroidはfixedThreadCountのプールを使用します。

AndroidのThread:ThreadからCoroutinesへ

Androidは低レベルのjava.lang.Threadから現代のコルーチンへと進化しました。抽象化の各レベルは、より少ないオーバーヘッドでより多くの機能を提供します。Threadは基本クラスですが、直接作成は推奨されません:新しいスレッドはプールで管理されず、監視やキャンセルが困難です。AsyncTask(API 30で非推奨)は一歩前進でしたが、メモリリークや設定処理の不便さに悩まされていました。

HandlerThread — Looperを持つThreadの特別なサブクラスで、メッセージキューを処理できます。これはバックグラウンドスレッドでの逐次的なタスク実行に使用され、例えばRoomやファイルへのデータ書き込みなどに使われます。HandlerThreadはstart()を呼び出して作成され、その後Handler(handlerThread.looper)を介してメッセージやRunnableを送信できます。handlerThread.quit()を呼び出すとLooperが停止し、スレッドが終了します。

kotlin
// 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:逐次バックグラウンドタスク

HandlerThread — 組み込みのLooperとメッセージキューを持つThreadの特殊化されたサブクラスです。start()を呼び出して作成され、その後Handler(handlerThread.looper)を介してRunnableやメッセージを送信できます。HandlerThreadはタスクを厳密に逐次的に実行します — 前のタスクが完了するまで次のタスクは開始されません。これは操作の順序が重要なRoomやファイルへのデータ書き込みに便利です。quitSafely()を呼び出すと、現在のタスク完了後にLooperが停止します。

iOSのThread:Thread、GCD、OperationQueue

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ライブラリとの統合などです。

swift
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 vs GCD:Threadを直接使用する場合

iOSでThreadを直接使用するのは3つの場合に正当化されます:スレッドローカルストレージ(Thread.current.threadDictionary) — スレッドにバインドされたデータの保存;バックグラウンドスレッドでperformSelector:onThread:を使用した特別なRunLoopの作成;pthread_tを期待するC/C++ライブラリとの統合。その他のすべての場合は、DispatchQueueを介したGCDが推奨されます — 自動的にスレッドプールと電力消費を管理します。

スレッドの同期:ロック、atomic、シリアルキュー

Race condition(競合状態)は、2つ以上のスレッドが同時に共有データにアクセスし、少なくとも1つのスレッドが書き込みを行うときに発生します。結果は実行の順序(タイミング)に依存し、予測不能です。競合状態を防ぐために同期プリミティブが使用されます。モバイル開発では、ロック(synchronized、NSLock)、アトミック操作(AtomicInteger、iOSのatomicプロパティ)、キュー(シリアルキュー)が利用可能です。

プリミティブの選択はシナリオに依存します。単純なカウンターやフラグにはアトミック操作(AtomicInteger、atomicプロパティ)で十分です。複数の操作を含むクリティカルセクションには — ロック(synchronized、NSLock)。複雑なデータ構造には — シリアルDispatchQueueまたはGCDバリア。ロックは理解しやすいですが、デッドロックやライブロックの影響を受けやすいです。キューはより複雑ですが、より安全です。

kotlin
// 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つのスレッドが異なる順序でロックを取得します。

スレッドプール:ExecutorsがThreadより優れている理由

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とは?

Thread — アプリケーションにおけるコード実行の基本単位です。各プロセスは複数のスレッドを持つことができ、メモリを共有しますが、独自のスタックを持ちます。モバイル開発では、UIをブロックせずにタスクを並列実行するためにスレッドが使用されます。AndroidはThread、Executors、HandlerThread、Coroutinesを使用します。iOSはThread、GCD(DispatchQueue)、OperationQueueを使用します。

Threadを直接作成することが推奨されない理由は?

Threadの作成にはAndroidで約1MB、iOSで約512KBのスタック割り当てが必要で、これは高コストな操作です。1000タスクに対して1000スレッドを直接作成すると、スタックだけで約1GB必要になり、さらにコンテキストスイッチのオーバーヘッドが加わります。Threadの代わりにプール(Executors、GCD)やコルーチンを使用してください — スレッドを再利用し、オーバーヘッドを数十倍削減します。

競合状態(race condition)とは?回避方法は?

Race condition — 複数のスレッドが同時に共有データに書き込みアクセスする際の予測不能な動作です。3つの方法で回避できます:アトミック型(AtomicInteger)の使用、ロック(synchronized、NSLock)の使用、キュー(シリアルDispatchQueue、KotlinのActor)によるアクセスの直列化。ベストプラクティスは共有mutable状態を最小限にし、イミュータビリティを使用することです。

Threadとコルーチンの違いは?

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を使用します。

まとめ

  • Thread — CPUの最小単位:独自スタックでの独立実行、ヒープメモリは共有
  • 5つの状態:New、Runnable、Running、Blocked/Waiting、Terminated
  • Androidの進化:Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOSが提供するもの:Thread、GCD(DispatchQueue)、OperationQueue — 低レベルから高レベルまで
  • Race conditionはロック(synchronized、NSLock)、アトミック型、シリアルキューで解決
  • Deadlockはロックの相互取得で発生 — 固定順序で防止
  • Thread Poolは新しいThread作成より効率的:スレッドを再利用し、コンテキストスイッチのオーバーヘッドを削減

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください