モバイル開発におけるThread Pool — 基礎、スレッドプール、動作原理

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

Thread Pool — 事前に作成されたスレッドのプールをタスクの実行に再利用し、スレッドの作成と破棄のオーバーヘッドを回避するスレッド管理メカニズムです。モバイル開発では、スレッドプールはバックグラウンド操作(ネットワークリクエスト、画像処理、データベース操作)に使用されます。Google Androidドキュメント(2025)によると、ExecutorServiceはAndroidでバックグラウンドスレッドを管理する推奨方法です。iOSでは、OperationQueueとグローバルconcurrentキューを持つGCD DispatchQueueが同様の役割を果たします。

重要なポイント

  • Thread Pool — スレッド作成のオーバーヘッドなしでバックグラウンドタスクを実行するための再利用可能なスレッドのプール。
  • ExecutorServiceはAndroidでThreadPoolExecutorを通じて設定可能なパラメータでプールを管理。
  • OperationQueueはiOSでmaxConcurrentOperationCountを通じてスレッドプールをカプセル化。
  • Core pool size — タスク実行のため常に準備ができているスレッドの最小数。
  • Work queueはプール内の空きスレッドを待つタスクを保存。

Thread Poolとは?

Thread Pool(スレッドプール)— 一定数のスレッドを事前に作成し、複数のタスクの実行に再利用するアーキテクチャパターンです。各操作ごとに新しいスレッドを作成する代わりに(JVMではスレッドあたり約1MBのスタックが必要で高コスト)、タスクはキューに配置され、プールの利用可能なスレッドによって実行されます。モバイル開発では、スレッドプールはパフォーマンスにとって重要です — AndroidとiOSはアプリケーションごとのスレッド数を制限しています。

モバイル開発でThread Poolが重要な理由

スレッドの作成は高コストな操作です:スタック割り当て、システム登録、コンテキストスイッチング。リソースが限られたモバイルデバイスでは、制御不能なスレッド作成はAndroidでOOM(OutOfMemoryError)、iOSでスロットリングを引き起こします。Thread Poolは両方の問題を解決します:同時実行スレッドの最大数を制限し、既存のスレッドを再利用します。Googleはraw Thread()の代わりにExecutorServiceを推奨し、AppleはThreadの代わりにOperationQueueを推奨しています。

パラメータプールなし(raw Thread)Thread Poolあり
スレッド作成タスクごとプール作成時に1回
最大スレッド数無制限(OOMリスク)core/max pool sizeで制限
利用率低い(タスク後スレッド終了)高い(スレッド再利用)
管理手動(join, interrupt)自動(ExecutorService)
メモリ消費タスクごとに増加固定

モバイル開発でThread Poolはどう機能する?

スレッドプールはProducer-Consumerの原則で動作します:タスク(Runnable/Callable)はブロッキングキュー(BlockingQueue)に配置されます。プールのスレッドはキューでタスクを待機し、実行のために取得します。アルゴリズム:空きスレッド数がcorePoolSize未満の場合、新しいスレッドが作成されます。corePoolSizeに達した場合、タスクはキューに配置されます。キューが満杯でスレッド数がmaximumPoolSize未満の場合、追加のスレッドが作成されます。maximumPoolSizeを超えた場合、タスクはRejectedExecutionHandlerを通じて拒否されます。

Core Pool Size vs Maximum Pool Size

Core pool size — アイドル時でもプールに保持されるスレッド数。Maximum pool size — キューオーバーフロー時に作成可能な最大スレッド数。これらの差は、一時的に作成されアイドルタイムアウト後に終了する追加(overflow)スレッドです。モバイルデバイスでは、スレッド作成によるピーク負荷を避けるため、corePoolSizeをmaximumPoolSizeと等しく設定することを推奨します。

Work QueueとRejectedExecutionHandler

BlockingQueueは実行待ちのタスクを保存します。最も一般的な実装:LinkedBlockingQueue(無制限)、ArrayBlockingQueue(制限付き)、SynchronousQueue(保存なし — タスクは直接スレッドに渡されます)。キューとプールが満杯になると、RejectedExecutionHandlerが起動します。標準ポリシー:AbortPolicy(RejectedExecutionExceptionをスロー)、CallerRunsPolicy(呼び出し元スレッドで実行)、DiscardPolicy、DiscardOldestPolicy。

kotlin
// AndroidでThread Poolを作成
val threadPool = ThreadPoolExecutor(
    corePoolSize = 2,        // 最小2スレッド
    maximumPoolSize = 4,     // 最大4スレッド
    keepAliveTime = 30L,     // オーバーフロースレッドの生存時間
    unit = TimeUnit.SECONDS,
    workQueue = LinkedBlockingQueue<Runnable>(16),
    threadFactory = Executors.defaultThreadFactory(),
    handler = ThreadPoolExecutor.CallerRunsPolicy()
)

// タスクの送信
threadPool.execute {
    val result = api.fetchData()
    runOnUiThread { showData(result) }
}

// プールのシャットダウン
threadPool.shutdown()
// 全タスク完了を待機
threadPool.awaitTermination(10, TimeUnit.SECONDS)

AndroidのThread Pool: ExecutorService

Androidはjava.util.concurrentを通じて複数のスレッドプール実装を提供します。Executors — 既成の設定を持つファクトリ:newFixedThreadPool(n)(固定プール)、newCachedThreadPool()(無制限、必要に応じてスレッド作成)、newSingleThreadExecutor()(単一スレッド — 逐次実行)。モバイルプロジェクトでは、キャッシュプールはスレッドを作りすぎる可能性があるため、適切な制限(2-4スレッド)のあるnewFixedThreadPoolを推奨します。

AndroidのThreadPoolExecutor

ThreadPoolExecutor(TPE)— 設定可能なパラメータを持つExecutorServiceの完全な実装。Androidでは、TPEはAsyncTask、IntentService、JobIntentServiceの内部で使用されます。corePoolSize、maximumPoolSize、keepAliveTime、BlockingQueue、RejectedExecutionHandlerのパラメータにより、プールの動作を細かく調整できます。Androidの推奨設定:corePoolSize = CPUコア数 - 1(IOバウンドタスクの場合)またはコア数(CPUバウンドタスクの場合)。一般的なアプリケーションの場合:2-4スレッド。

kotlin
// Executorsの既成設定
// 1. 3スレッドの固定プール
val fixedPool = Executors.newFixedThreadPool(3)

// 2. キャッシュプール(モバイル非推奨)
val cachedPool = Executors.newCachedThreadPool()

// 3. 単一スレッド(シリアル化)
val singlePool = Executors.newSingleThreadExecutor()

// 4. スケジューラ(定期タスク)
val scheduler = Executors.newScheduledThreadPool(2)

// CallableとFutureの使用
val future: Future<String> = fixedPool.submit(Callable {
    "Result: ${api.call()}"
})
// 結果の取得(スレッドをブロック)
val result = future.get(5, TimeUnit.SECONDS)

// プールのシャットダウン
fixedPool.shutdownNow()

スレッドプールとしてのCoroutineDispatcher

KotlinコルーチンはCoroutineDispatcherを提供します — スレッドプールに似た抽象化です。Dispatchers.IOは64スレッドのプール(制限付き)を使用します。Dispatchers.Default — CPUコア数と等しいプール。CoroutineDispatcherは手動シャットダウンを必要とせず、自動的に管理されます。細かい調整には、Executors.newFixedThreadPool(2).asCoroutineDispatcher()を通じてカスタムExecutorCoroutineDispatcherを作成します。コルーチンはスレッドプールを置き換えるのではなく、ラップします。

iOSのThread Pool: OperationQueueとGCD

iOSはスレッドプール管理のための2つの主要なメカニズムを提供します:OperationQueue(GCD上に構築された高レベルAPI)とGCD DispatchQueue(低レベルC-API)。OperationQueueはmaxConcurrentOperationCountプロパティを通じてスレッドプールをカプセル化します。デフォルトでは、OperationQueueはシステム定義の最大値(システム負荷に依存)を使用します。DispatchQueue.global()はシステムスレッドプールを持つconcurrentキューを提供します。

OperationQueueとmaxConcurrentOperationCount

OperationQueueはmaxConcurrentOperationCountを通じてスレッドプールを管理します。値1はシリアルキュー(単一スレッドプールに類似)を作成します。1より大きい値は指定された制限を持つconcurrentプールを作成します。デフォルトでは、maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount(システム最適値、通常4-8スレッド)。Operationは依存関係、優先順位、キャンセルをサポートします。各操作はシステムプールの利用可能なスレッドで実行されます。

swift
// 3スレッドプールのOperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 3
queue.qualityOfService = .utility

// 操作の作成
let operation1 = BlockOperation {
    let data = fetchData(from: url1)
    DispatchQueue.main.async { updateUI(data) }
}

let operation2 = BlockOperation {
    let data = fetchData(from: url2)
    DispatchQueue.main.async { updateUI(data) }
}

// 依存関係:operation2はoperation1を待機
operation2.addDependency(operation1)

// キューへの追加
queue.addOperations([operation1, operation2], waitUntilFinished: false)

// 全操作のキャンセル
queue.cancelAllOperations()

スレッドプールとしてのGCD DispatchQueue

DispatchQueue — Appleのスレッドプールです。concurrentキュー(qos: .utility)は、現在のデバイス負荷に最適化されたシステムスレッドプールを使用します。異なるQoSレベル(userInteractive、userInitiated、utility、background)は異なる優先順位を持つ異なるプールにマッピングされます。DispatchGroupは複数のタスクを同期できます。DispatchWorkItemはキャンセルとqualityOfServiceをサポートします。細かい制御には、DispatchQueue(label: qos: attributes: .concurrent)を通じてカスタムconcurrentキューを作成します。

swift
// スレッドプールとしてのGCD DispatchQueue
let customQueue = DispatchQueue(
    label: "com.app.background",
    qos: .utility,
    attributes: .concurrent,
    autoreleaseFrequency: .workItem
)

// プールへのタスク送信
customQueue.async { self.processFile(file1) }
customQueue.async { self.processFile(file2) }

// 同期のためのDispatchGroup
let group = DispatchGroup()
let pool = DispatchQueue.global(qos: .utility)

pool.async(group: group) { fetchData() }
pool.async(group: group) { processImage() }

group.notify(queue: .main) {
    self.showResult() // 両方のタスク完了
}

// セマフォによる並行性制限
let semaphore = DispatchSemaphore(value: 3)
for url in urls {
    pool.async {
        semaphore.wait()
        download(url)
        semaphore.signal()
    }
}

Thread Pool設定パラメータ

スレッドプールの設定はアプリケーションのパフォーマンスに直接影響します。誤ったパラメータはCPUの過小利用(スレッドが少なすぎる)またはシステム過負荷(多すぎる)を引き起こします。モバイルアプリケーションでは、リソース制限と消費電力のため、最適値はサーバーサイドとは異なります。主なパラメータ:corePoolSize、maxPoolSize、キュー容量、keepAliveTime。

最適なプールサイズの計算

IOバウンドタスクの公式:corePoolSize = CPUコア数 × 2(スレッドはI/O待機)。CPUバウンドタスクの場合:corePoolSize = CPUコア数(スレッドは計算で常にビジー)。最新のモバイルデバイス(6-8コア)では、CPUバウンドで6-8スレッド、IOバウンドで12-16スレッドになります。実践的なテストでは、一般的なモバイルアプリケーションでは3-4スレッドが最適です — それ以上スレッドを増やしてもパフォーマンス向上なく消費電力が増加します。

キュー容量とオーバーフロー動作

ワークキューのサイズは、実行を待機できるタスク数を決定します。無制限キュー(制限なしのLinkedBlockingQueue)はタスクの急速な到着時にOOMを引き起こす可能性があります。制限付きキュー(固定サイズのArrayBlockingQueue)は満杯時にタスクを拒否します。モバイルアプリケーションには、16-32タスクの容量を持つArrayBlockingQueueを推奨します。CallerRunsPolicyはモバイルに最適なRejectedExecutionHandlerです:タスクを失う代わりに呼び出し元を遅くします(バックプレッシャー)。

パラメータモバイル推奨値理由
corePoolSize2-4モバイルデバイスの限られたリソース
maxPoolSizecorePoolSize(または+1-2)スレッド作成によるピーク負荷回避
keepAliveTime15-30秒頻繁な作成なしで迅速なメモリ解放
キュー容量16-32バッファリングとOOMリスクのバランス
ハンドラCallerRunsPolicyタスクを失わないバックプレッシャー

スレッドプール使用時のよくある間違い

モバイルアプリケーション開発者はスレッドプール使用時に、クラッシュ、メモリリーク、不安定な動作の原因となる間違いをよく犯します。最も一般的なもの:ExecutorServiceでshutdown()を呼び出さない、操作ごとに新しいプールを作成する、プールが大きすぎる、タスク間のデッドロック、AndroidでCachedThreadPoolを使用する。

Thread Poolのデッドロック

デッドロックは、プール内のタスクが同じプールの別のタスクの結果を待っているが、すべてのスレッドが待機中の場合に発生します。:タスクAが同じプールにタスクBを送信し、future.get()を呼び出す — プールが枯渇すると、タスクAはタスクBを待ち、タスクBは空きスレッドがないため実行できません。解決策:異なるレベルのタスクに別々のプールを使用するか、ブロッキング.get()の代わりに非同期コールバックを使用します。

kotlin
// Thread Poolのデッドロック
val pool = Executors.newFixedThreadPool(1)

// タスクAがタスクBを待機 — デッドロック!
val futureA = pool.submit {
    // このタスクは決して実行されない
    val futureB = pool.submit { 42 }
    futureB.get() // 永久にブロック
}

// 解決策:別々のプール
val workerPool = Executors.newFixedThreadPool(2)
val callbackPool = Executors.newSingleThreadExecutor()

workerPool.submit {
    callbackPool.submit {
        // 別のプールで実行 — デッドロック不可能
    }
}

// またはCompletableFutureを使用
workerPool.submit {
    CompletableFuture
        .supplyAsync { 42 }
        .thenAccept { result ->
            println(result)
        }
}

未終了のプールとリーク

Activityで作成されたExecutorServiceはonDestroy()でシャットダウンする必要があります。これを行わないと、Activityが破棄された後もスレッドがメモリに残ります。解決策:プールはActivityではなく、ApplicationスコープまたはViewModelに保持します。コルーチンの場合は、viewModelScopeまたはlifecycleScopeを使用します。Activity内でプールを作成した場合は、onDestroy()でpool.shutdown()を必ず呼び出します。テストでは、即時停止にes.shutdownNow()を使用します。

よくある質問

Thread Poolと通常のスレッドの違いは?

Thread Poolは既存のスレッドを再利用して複数のタスクを実行します。通常のスレッド(raw Thread)は作成され、1つのタスクを実行し、破棄されます。スレッドの作成には約1MBのメモリと約1msの時間がかかります。Thread Poolはオーバーヘッドを減らし、最大スレッド数を制限し、管理用API(shutdown、awaitTermination)を提供します。

モバイルアプリケーションのプールにはいくつのスレッドが必要?

一般的なモバイルアプリケーションには、2-4スレッドが最適です。CPUバウンドタスクの場合はCPUコア数、IOバウンドタスクの場合はコア数 × 2。スレッドを増やしてもパフォーマンス向上はなく、消費電力とコンテキストスイッチングが増加します。AndroidではProcess.availableProcessors()、iOSではProcessInfo.processInfo.processorCountを使用してコア数を確認します。

CachedThreadPoolとは?なぜAndroidで危険?

CachedThreadPoolは必要に応じてスレッドを作成し、既存のスレッドを再利用します。問題:最大スレッド数を制限しません。100のタスクが同時に到着すると、100のスレッドが作成されます。これはAndroidでOOMを引き起こします(各スレッド約1MB)。明示的な制限付きのnewFixedThreadPool(n)を使用してください。CachedThreadPoolは少量が保証された短期バーストタスクにのみ許容されます。

ExecutorServiceでshutdown()を呼び出す必要はある?

はい、プールが管理対象コンテナ(コルーチンなど)に属していない場合。shutdown()は新しいタスクの受け入れを停止し、現在のタスク完了後にスレッドを終了します。shutdown()がないと、スレッドはメモリに残り、アプリケーションは終了しません。Activityの場合はonDestroy()で呼び出します。ViewModelの場合はcoroutineScopeを使用します。プールのシャットダウンは、CursorやInputStreamのクローズと同様にリソース管理の必須部分です。

OperationQueueとDispatchQueueはスレッドプール?

はい、OperationQueueとDispatchQueueはiOSが提供するスレッドプールです。OperationQueueはmaxConcurrentOperationCountで並行性を制限します。DispatchQueue.global()は直接制御なしでシステムスレッドプールを使用します。JavaのThreadPoolExecutorとは異なり、corePoolSizeやキュー容量を管理しません — システムが現在の負荷とデバイスの消費電力に基づいてプールを自動最適化します。

まとめ

  • Thread Pool — バックグラウンドタスク用の再利用可能なスレッドプール、スレッド作成のオーバーヘッドを削減。
  • AndroidはThreadPoolExecutorと明示的なプールサイズ制限付きExecutors.newFixedThreadPool(n)を使用。
  • iOSはmaxConcurrentOperationCount付きOperationQueueとQoSプール付きGCD DispatchQueueを提供。
  • Core pool size — 最小スレッド数、maximum pool size — キューオーバーフロー時の最大数。
  • デッドロックはタスクが同じプールの別のタスクを待機してブロックされると発生。
  • CallerRunsPolicyはモバイルアプリに推奨 — タスクを失わずに呼び出し元を減速。
  • 一般的なモバイルアプリの最適プールサイズは2-4スレッド、クリーンアップにshutdown()を使用。

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

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

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

こちらもお読みください