Thread Pool — 事前に作成されたスレッドのプールをタスクの実行に再利用し、スレッドの作成と破棄のオーバーヘッドを回避するスレッド管理メカニズムです。モバイル開発では、スレッドプールはバックグラウンド操作(ネットワークリクエスト、画像処理、データベース操作)に使用されます。Google Androidドキュメント(2025)によると、ExecutorServiceはAndroidでバックグラウンドスレッドを管理する推奨方法です。iOSでは、OperationQueueとグローバルconcurrentキューを持つGCD DispatchQueueが同様の役割を果たします。
重要なポイント
Thread Pool(スレッドプール)— 一定数のスレッドを事前に作成し、複数のタスクの実行に再利用するアーキテクチャパターンです。各操作ごとに新しいスレッドを作成する代わりに(JVMではスレッドあたり約1MBのスタックが必要で高コスト)、タスクはキューに配置され、プールの利用可能なスレッドによって実行されます。モバイル開発では、スレッドプールはパフォーマンスにとって重要です — AndroidとiOSはアプリケーションごとのスレッド数を制限しています。
スレッドの作成は高コストな操作です:スタック割り当て、システム登録、コンテキストスイッチング。リソースが限られたモバイルデバイスでは、制御不能なスレッド作成は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) |
| メモリ消費 | タスクごとに増加 | 固定 |
スレッドプールはProducer-Consumerの原則で動作します:タスク(Runnable/Callable)はブロッキングキュー(BlockingQueue)に配置されます。プールのスレッドはキューでタスクを待機し、実行のために取得します。アルゴリズム:空きスレッド数がcorePoolSize未満の場合、新しいスレッドが作成されます。corePoolSizeに達した場合、タスクはキューに配置されます。キューが満杯でスレッド数がmaximumPoolSize未満の場合、追加のスレッドが作成されます。maximumPoolSizeを超えた場合、タスクはRejectedExecutionHandlerを通じて拒否されます。
Core pool size — アイドル時でもプールに保持されるスレッド数。Maximum pool size — キューオーバーフロー時に作成可能な最大スレッド数。これらの差は、一時的に作成されアイドルタイムアウト後に終了する追加(overflow)スレッドです。モバイルデバイスでは、スレッド作成によるピーク負荷を避けるため、corePoolSizeをmaximumPoolSizeと等しく設定することを推奨します。
BlockingQueueは実行待ちのタスクを保存します。最も一般的な実装:LinkedBlockingQueue(無制限)、ArrayBlockingQueue(制限付き)、SynchronousQueue(保存なし — タスクは直接スレッドに渡されます)。キューとプールが満杯になると、RejectedExecutionHandlerが起動します。標準ポリシー:AbortPolicy(RejectedExecutionExceptionをスロー)、CallerRunsPolicy(呼び出し元スレッドで実行)、DiscardPolicy、DiscardOldestPolicy。
// 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はjava.util.concurrentを通じて複数のスレッドプール実装を提供します。Executors — 既成の設定を持つファクトリ:newFixedThreadPool(n)(固定プール)、newCachedThreadPool()(無制限、必要に応じてスレッド作成)、newSingleThreadExecutor()(単一スレッド — 逐次実行)。モバイルプロジェクトでは、キャッシュプールはスレッドを作りすぎる可能性があるため、適切な制限(2-4スレッド)のあるnewFixedThreadPoolを推奨します。
ThreadPoolExecutor(TPE)— 設定可能なパラメータを持つExecutorServiceの完全な実装。Androidでは、TPEはAsyncTask、IntentService、JobIntentServiceの内部で使用されます。corePoolSize、maximumPoolSize、keepAliveTime、BlockingQueue、RejectedExecutionHandlerのパラメータにより、プールの動作を細かく調整できます。Androidの推奨設定:corePoolSize = CPUコア数 - 1(IOバウンドタスクの場合)またはコア数(CPUバウンドタスクの場合)。一般的なアプリケーションの場合:2-4スレッド。
// 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()
KotlinコルーチンはCoroutineDispatcherを提供します — スレッドプールに似た抽象化です。Dispatchers.IOは64スレッドのプール(制限付き)を使用します。Dispatchers.Default — CPUコア数と等しいプール。CoroutineDispatcherは手動シャットダウンを必要とせず、自動的に管理されます。細かい調整には、Executors.newFixedThreadPool(2).asCoroutineDispatcher()を通じてカスタムExecutorCoroutineDispatcherを作成します。コルーチンはスレッドプールを置き換えるのではなく、ラップします。
iOSはスレッドプール管理のための2つの主要なメカニズムを提供します:OperationQueue(GCD上に構築された高レベルAPI)とGCD DispatchQueue(低レベルC-API)。OperationQueueはmaxConcurrentOperationCountプロパティを通じてスレッドプールをカプセル化します。デフォルトでは、OperationQueueはシステム定義の最大値(システム負荷に依存)を使用します。DispatchQueue.global()はシステムスレッドプールを持つconcurrentキューを提供します。
OperationQueueはmaxConcurrentOperationCountを通じてスレッドプールを管理します。値1はシリアルキュー(単一スレッドプールに類似)を作成します。1より大きい値は指定された制限を持つconcurrentプールを作成します。デフォルトでは、maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount(システム最適値、通常4-8スレッド)。Operationは依存関係、優先順位、キャンセルをサポートします。各操作はシステムプールの利用可能なスレッドで実行されます。
// 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()
DispatchQueue — Appleのスレッドプールです。concurrentキュー(qos: .utility)は、現在のデバイス負荷に最適化されたシステムスレッドプールを使用します。異なるQoSレベル(userInteractive、userInitiated、utility、background)は異なる優先順位を持つ異なるプールにマッピングされます。DispatchGroupは複数のタスクを同期できます。DispatchWorkItemはキャンセルとqualityOfServiceをサポートします。細かい制御には、DispatchQueue(label: qos: attributes: .concurrent)を通じてカスタムconcurrentキューを作成します。
// スレッドプールとしての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()
}
}
スレッドプールの設定はアプリケーションのパフォーマンスに直接影響します。誤ったパラメータは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です:タスクを失う代わりに呼び出し元を遅くします(バックプレッシャー)。
| パラメータ | モバイル推奨値 | 理由 |
|---|---|---|
| corePoolSize | 2-4 | モバイルデバイスの限られたリソース |
| maxPoolSize | corePoolSize(または+1-2) | スレッド作成によるピーク負荷回避 |
| keepAliveTime | 15-30秒 | 頻繁な作成なしで迅速なメモリ解放 |
| キュー容量 | 16-32 | バッファリングとOOMリスクのバランス |
| ハンドラ | CallerRunsPolicy | タスクを失わないバックプレッシャー |
モバイルアプリケーション開発者はスレッドプール使用時に、クラッシュ、メモリリーク、不安定な動作の原因となる間違いをよく犯します。最も一般的なもの:ExecutorServiceでshutdown()を呼び出さない、操作ごとに新しいプールを作成する、プールが大きすぎる、タスク間のデッドロック、AndroidでCachedThreadPoolを使用する。
デッドロックは、プール内のタスクが同じプールの別のタスクの結果を待っているが、すべてのスレッドが待機中の場合に発生します。例:タスクAが同じプールにタスクBを送信し、future.get()を呼び出す — プールが枯渇すると、タスクAはタスクBを待ち、タスクBは空きスレッドがないため実行できません。解決策:異なるレベルのタスクに別々のプールを使用するか、ブロッキング.get()の代わりに非同期コールバックを使用します。
// 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は既存のスレッドを再利用して複数のタスクを実行します。通常のスレッド(raw Thread)は作成され、1つのタスクを実行し、破棄されます。スレッドの作成には約1MBのメモリと約1msの時間がかかります。Thread Poolはオーバーヘッドを減らし、最大スレッド数を制限し、管理用API(shutdown、awaitTermination)を提供します。
一般的なモバイルアプリケーションには、2-4スレッドが最適です。CPUバウンドタスクの場合はCPUコア数、IOバウンドタスクの場合はコア数 × 2。スレッドを増やしてもパフォーマンス向上はなく、消費電力とコンテキストスイッチングが増加します。AndroidではProcess.availableProcessors()、iOSではProcessInfo.processInfo.processorCountを使用してコア数を確認します。
CachedThreadPoolは必要に応じてスレッドを作成し、既存のスレッドを再利用します。問題:最大スレッド数を制限しません。100のタスクが同時に到着すると、100のスレッドが作成されます。これはAndroidでOOMを引き起こします(各スレッド約1MB)。明示的な制限付きのnewFixedThreadPool(n)を使用してください。CachedThreadPoolは少量が保証された短期バーストタスクにのみ許容されます。
はい、プールが管理対象コンテナ(コルーチンなど)に属していない場合。shutdown()は新しいタスクの受け入れを停止し、現在のタスク完了後にスレッドを終了します。shutdown()がないと、スレッドはメモリに残り、アプリケーションは終了しません。Activityの場合はonDestroy()で呼び出します。ViewModelの場合はcoroutineScopeを使用します。プールのシャットダウンは、CursorやInputStreamのクローズと同様にリソース管理の必須部分です。
はい、OperationQueueとDispatchQueueはiOSが提供するスレッドプールです。OperationQueueはmaxConcurrentOperationCountで並行性を制限します。DispatchQueue.global()は直接制御なしでシステムスレッドプールを使用します。JavaのThreadPoolExecutorとは異なり、corePoolSizeやキュー容量を管理しません — システムが現在の負荷とデバイスの消費電力に基づいてプールを自動最適化します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。