Background Thread — ユーザーインターフェースに関連しない実行スレッドで、長時間の操作(ネットワークリクエスト、ファイル操作、JSON解析、画像圧縮、暗号化、データベースクエリ)用に設計されています。iOSでは、バックグラウンドスレッドはGCD(DispatchQueue.global)とOperationQueueで管理されます。Androidでは、Executors、WorkManager、Kotlin Coroutines(Dispatchers.IO、Dispatchers.Default)で管理されます。Apple DispatchQueue ドキュメントによると、バックグラウンド操作が完了したら、インターフェースを更新するために結果をMain Threadに返す必要があります。
重要ポイント
Background Thread — アプリケーション内でMain Threadではなく、UIにアクセスできないスレッドのことです。その役割は、メインスレッドから重い操作を解放し、インターフェースの応答性を維持することです。オペレーティングシステムはバックグラウンドスレッドをCPUコアに分散し、複数のタスクを並行して実行できるようにします。iOSはGCD、AndroidはJava Executorsプールを通じてスレッドプールを自動的に管理します。
イベントを順次(一つずつ)処理するMain Threadとは異なり、バックグラウンドスレッドはCPUコア数の制限のみで並行実行できます。例えば、8コアデバイスでは最大8つの並行バックグラウンドタスクを大幅な遅延なく実行できます。ただし、過剰な数のスレッド(数百)はthread starvation — コアの競合とコンテキストスイッチのオーバーヘッド増大を引き起こします。
Quality of Service(QoS) — iOSのメカニズムで、バックグラウンドタスクの優先度を指定できます。値:.userInteractive(最高、ほぼMain Thread)、.userInitiated(ユーザーが結果を待つ)、.default(標準)、.utility(ユーザーが直接待たない)、.background(最低、同期とインデックス用)。Androidでは、Thread.setPriority()(1〜10)が相当しますが、Androidはcgroupsを使用したスレッド優先度のグループ管理も行います。
DispatchQueue.global(qos:) — iOSでバックグラウンドキューを取得する主要な方法です。GCD(Grand Central Dispatch)は自動的にスレッドプールを作成し、タスクをコアに分散します。DispatchQueue.global(qos: .background).async {}の呼び出しは、最低優先度のバックグラウンドキューにブロックを送信します。結果が即座に必要なタスクには、.userInitiatedまたは.utilityを使用します。
OperationQueue — GCD上の高レベル抽象化で、操作間の依存関係、最大同時実行操作数(maxConcurrentOperationCount)、優先度を設定できます。OperationQueueは複雑なマルチタスクチェーン(ファイルダウンロード→解凍→キャッシュ保存)に便利です。デフォルトでは、OperationQueueは特に指定がない限りバックグラウンドスレッドを使用します。
import UIKit
class ImageDownloader {
func downloadImagesSequentially() {
let urls = ["https://example.com/1.png", "https://example.com/2.png"]
// maxConcurrentOperationCount = 2のOperationQueue
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
queue.qualityOfService = .utility
for urlString in urls {
queue.addOperation {
guard let url = URL(string: urlString),
let data = try? Data(contentsOf: url)
else { return }
DispatchQueue.main.async {
print("読み込み完了: \(url.lastPathComponent)")
}
}
}
}
// GCD:異なるQoSを持つグローバルバックグラウンドキュー
func backgroundTaskWithQoS() {
DispatchQueue.global(qos: .userInitiated).async {
// 高優先度 — ユーザーが結果を待っている
let result = self.heavyComputation()
DispatchQueue.main.async {
self.showResult(result)
}
}
}
private func heavyComputation() -> String {
Thread.sleep(forTimeInterval: 2) // 作業のシミュレーション
return "計算結果"
}
private func showResult(_ result: String) {
print("Result on Main: \(result)")
}
}
この例では、OperationQueueがqualityOfService = .utilityによるバックグラウンドQoSで2つの画像を並行読み込みします(maxConcurrentOperationCount = 2)。GCDメソッドのbackgroundTaskWithQoSは、ユーザーが結果を待つタスクに.userInitiatedでグローバルキューを使用します。両方のアプローチはDispatchQueue.mainに戻ってUIを更新することで終了します。これはiOSでの必須要件です。
GCDはシリアルキューとコンカレントキューの2種類をサポートします。シリアルキューはタスクを一つずつ実行します。これはロックなしで共有リソース(ファイル、DB)にアクセスするのに便利です。コンカレントキューはタスクを並行実行し、利用可能なコアに分散します。DispatchQueue.globalは常にコンカレントです。シリアルキューを作成するには、DispatchQueue(label: "com.app.queue")を使用します。
Androidはバックグラウンドスレッドに複数の抽象化レベルを提供します。クラシックなアプローチはjava.util.concurrent.Executors.newFixedThreadPool(n)またはExecutors.newCachedThreadPool()です。モダンなアプローチはDispatchers.IO(I/O:ネットワーク、ファイル、DB)とDispatchers.Default(CPU負荷の高いタスク:ソート、画像処理)を備えたKotlin Coroutinesです。WorkManagerは遅延実行と保証されたバックグラウンドタスク用です。
HandlerThread — 独自のLooper(メッセージキュー)を持つバックグラウンドスレッドを作成するためのAndroidの特殊クラスです。Executorsとは異なり、HandlerThreadはHandlerを介してメッセージとRunnableを送信できます。キューイングが必要な操作(例:シーケンシャルなDB書き込み)に使用されます。使用後はquit()またはquitSafely()を呼び出してリソースを解放する必要があります。
// Android:ExecutorsとCoroutines Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
class DataRepository {
private val ioExecutor = Executors.newFixedThreadPool(4)
// Executorsによるクラシックなアプローチ
fun loadDataLegacy(callback: (String) -> Unit) {
ioExecutor.execute {
val result = readFromFile()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
handler.post { callback(result) }
}
}
// Coroutinesによるモダンなアプローチ
suspend fun loadDataCoroutines(): String {
return withContext(Dispatchers.IO) {
// ファイル操作 — バックグラウンドプールで実行中
readFromFile()
}
// 結果は自動的にDispatchers.Mainに返される
}
// Dispatchers.DefaultでのCPU負荷の高いタスク
suspend fun processImage(pixels: IntArray): IntArray {
return withContext(Dispatchers.Default) {
// ソート、フィルタリング — Defaultプールで実行中
pixels.sortedArray()
}
}
private fun readFromFile(): String {
Thread.sleep(1000) // ファイル読み込みのシミュレーション
return "file_content"
}
fun cleanup() {
ioExecutor.shutdown()
}
}
DataRepositoryの例は、Androidでのバックグラウンドスレッドの進化を示しています。レガシーメソッドloadDataLegacyは、Main Threadに戻るためにHandlerと共にExecutors.newFixedThreadPool(4)を使用します。モダンなloadDataCoroutinesはwithContext(Dispatchers.IO)を使用します。coroutineはスレッドをブロックせずに実行中は一時停止し、自動的にMain Threadで再開します。Dispatchers.DefaultはCPUバウンドの操作(ソート、フィルタリング、データ変換)に推奨されます。
Kotlin Coroutines — 単なるスレッド処理の方法ではなく、根本的に異なるモデルです。非同期タスクは特定のスレッドに紐づかず、ブロックなしで一時停止できます。つまり、バックグラウンドにある間、coroutineはスレッドを占有せず、他のタスクに解放します。一時停止メカニズムにより、4〜8スレッドのプールでthread starvationなしに数十万の同時タスクを実行できます。
3つの主要なディスパッチャ:Dispatchers.Main(UI、1スレッド)、Dispatchers.IO(デフォルト64スレッド、ブロッキング操作用:ネットワーク、ファイル、DB)、Dispatchers.Default(CPUコア数と同数、高負荷計算用)。withContextでこれらを組み合わせることで、コールバックを作成せずにスレッド間を切り替えられます。withContextはタスク完了まで制御を返さないsuspend関数です。
// Coroutines:バックグラウンドタスクの構成
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
suspend fun loadUserProfile(userId: String): UserProfile =
coroutineScope {
// 異なるソースからの並行データ読み込み
val user = async(Dispatchers.IO) { fetchUser(userId) }
val posts = async(Dispatchers.IO) { fetchPosts(userId) }
val avatar = async(Dispatchers.Default) {
processAvatar(fetchAvatar(userId))
}
// await() — すべてのタスク完了まで一時停止
UserProfile(
user = user.await(),
posts = posts.await(),
avatar = avatar.await()
)
}
data class UserProfile(
val user: String,
val posts: List<String>,
val avatar: ByteArray
)
suspend fun fetchUser(id: String): String { delay(300); return "User:$id" }
suspend fun fetchPosts(id: String): List<String> { delay(500); return listOf("Post1") }
suspend fun fetchAvatar(id: String): ByteArray { delay(200); return ByteArray(1024) }
suspend fun processAvatar(data: ByteArray): ByteArray { delay(100); return data }
loadUserProfile関数は、asyncを介して3つの並行バックグラウンドタスクを起動します。fetchUserとfetchPostsはIOバウンド(ネットワーク)でDispatchers.IOで実行されます。processAvatarはCPUバウンド(画像処理)でDispatchers.Defaultで実行されます。await()はすべてのタスクが完了するまでcoroutineを一時停止します。総実行時間は3つのタスクの最大時間(fetchPostsで500 ms)と等しく、合計ではありません。これが逐次実行に対するcoroutinesの主要な利点です。
Structured concurrency — 各coroutineが親スコープを持ち、親のキャンセルが子coroutineを自動的にキャンセルするという原則です。Androidでは、lifecycleScopeがActivity破棄時にすべてのcoroutineをキャンセルします。viewModelScopeはViewModelクリア時に行います。これによりバックグラウンドタスクのリークを防止します。ユーザーが画面を閉じた場合、バックグラウンドにあるcoroutineは不要になったデータの読み込みを続行しません。
WorkManager — デバイスの再起動やアプリ終了後でも完了しなければならないバックグラウンドタスクを実行するためのAndroid Jetpackライブラリです。アプリプロセス内で動作するExecutorsやcoroutinesとは異なり、WorkManagerはタスクをシステムディスパッチャに引き渡し、適切な条件(ネットワーク可用性、バッテリー残量、空き容量)での実行を保証します。WorkManagerはデータ同期、ログアップロード、バックアップに適しています。
WorkManagerのタスクはWorker(またはcoroutines用のCoroutineWorker)を拡張するクラスです。Worker.doWork()はWorkManagerが提供するバックグラウンドスレッドで実行されます。結果はResult.success()、Result.retry()、Result.failure()で返されます。タスクはチェーンできます:oneTimeWorkRequest.andThen(nextRequest).enqueue()。WorkManagerは制約(Constraints)を考慮して最適な実行時間を選択します。
// Coroutinesを使用したWorkManager
import android.content.Context
import androidx.work.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class SyncWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// Dispatchers.Defaultで実行中(デフォルト)
return withContext(Dispatchers.IO) {
try {
syncDataToServer()
Result.success()
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
private suspend fun syncDataToServer() {
// 同期のシミュレーション
delay(1000)
}
}
// 制約付きWorkManagerタスクの起動
fun scheduleSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncWork = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, java.util.concurrent.TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context).enqueue(syncWork)
}
SyncWorkerはcoroutinesをサポートするWorkerのバージョンであるCoroutineWorkerを拡張します。doWork()はDispatchers.Defaultで実行され、withContextを介してネットワーク操作用にIOに切り替わります。Constraintsは、ネットワークが利用可能でバッテリー残量が低下していない場合にのみ同期が実行されることを保証します。EXPONENTIALを使用したBackoffCriteriaは再試行間隔を増加させます:10、20、40秒。
定期的なタスク(15分ごとの同期、1時間ごとの分析送信)には、WorkManagerはPeriodicWorkRequestBuilderを提供します。最小間隔は15分です。OneTimeWorkRequestとは異なり、PeriodicWorkRequestは正確な間隔の順守を保証しません。システムはバッテリー節約のために複数の定期タスクをバッチ処理する可能性があります。正確な間隔が必要な場合はAlarmManagerを使用しますが、Android 12+の正確なアラームに関する制限に注意してください。
最初の間違い — タスクごとに新しいThreadを作成すること。new Thread().start()はスタックに約1 MBを割り当てるネイティブスレッドを作成します。100の並行タスクの場合、スタックだけで100 MBになり、さらにコンテキストスイッチのオーバーヘッドが加わります。スレッドプール(AndroidではExecutors.newFixedThreadPool(n)、iOSではDispatchQueue.global())を使用しましょう。スレッドを再利用し、オーバーヘッドを桁違いに削減します。
2番目の間違い — 同期なしで複数のバックグラウンドスレッドからミュータブルな状態にアクセスすること。2つのバックグラウンドスレッドが同時に同じArrayListやHashMapに書き込むと、競合状態が発生します:AndroidではConcurrentModificationException、iOSではデータ破損。解決策:スレッドセーフなコレクション(ConcurrentHashMap、CopyOnWriteArrayList)を使用するか、単一キュー(DispatchQueue serial)でアクセスを直列化します。
3番目の間違い — ライフサイクル管理なしのバックグラウンドタスク。ActivityやViewModelのライフサイクルにバインドせずにグローバルスコープでcoroutineを起動するとリークが発生します:画面破棄後もタスクが実行され続けます。AndroidではlifecycleScope(Activity/Fragment)またはviewModelScope(ViewModel)を使用します。iOSではクロージャでweak selfを使用し、deinitでタスクをキャンセルします。
よくある質問
Background Thread — UIに関係ない操作(ネットワークリクエスト、ファイル読み書き、JSON解析、計算)を実行するスレッドです。Main Threadを重い処理から解放し、インターフェースの応答性を維持します。iOSではGCD(DispatchQueue.global)で、AndroidではExecutorsまたはKotlin Coroutines(Dispatchers.IO、Dispatchers.Default)でバックグラウンドスレッドを管理します。
Dispatchers.IOはブロッキングI/O操作(ファイル読み込み、ネットワークリクエスト、DB操作)用に設計されています。64スレッドのプールを持ちます。Dispatchers.DefaultはCPU負荷の高いタスク(ソート、フィルタリング、画像処理)用です。プールはCPUコア数と同数です。I/O操作にDispatchers.Defaultを使用すると全コアがブロックされ、CPUタスクにDispatchers.IOを使用すると過剰なスレッドが作成される可能性があります。
DispatchQueue.global(qos: .background).async { }でグローバルバックグラウンドキューにブロックを送信します。バックグラウンド処理完了後、DispatchQueue.main.async { }でメインスレッドに戻りUIを更新します。シーケンシャルなバックグラウンドタスクには、maxConcurrentOperationCount = 1のOperationQueueまたはDispatchQueue(label: "serial")を使用します。
推奨されるバックグラウンドスレッド数はCPUコア数にIOバウンドタスク用の1を加えた数です。最新の8コアデバイスでは9スレッドです。数百のスレッドを作成するとthread starvationが発生します:OSがタスク実行よりもコンテキストスイッチに多くの時間を費やします。iOSのGCDとAndroidのExecutorsは現在のデバイスに合わせてスレッドプールを自動最適化します。
Kotlin Coroutinesでは、coroutineがMainスコープ(lifecycleScope.launch、viewModelScope.launch)で起動された場合、Main Threadへの復帰は自動的に行われます。withContext(Dispatchers.IO)関数はIOスレッドでcoroutineを一時停止し、完了後に起動元のディスパッチャ(通常はMain)で自動的に再開します。DispatchQueue.main.asyncの明示的な呼び出しは不要です。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。