Thread — базовая единица процессорного времени, имеющая собственный стек и выполняющаяся независимо от других потоков. В мобильной разработке потоки используются для параллельного выполнения задач, чтобы интерфейс оставался отзывчивым во время длительных операций. Android поддерживает java.lang.Thread, Executors и Kotlin Coroutines, iOS — Thread (Objective-C), GCD и OperationQueue. По данным Android Thread Documentation, создание нативного потока требует выделения ~1 МБ под стек операционной системой.
Главное
Thread (поток выполнения) — это независимая последовательность инструкций, которую операционная система может запланировать на ядре CPU. Каждый процесс (приложение) содержит как минимум один поток — Main Thread. Дополнительные потоки создаются для параллельного выполнения задач. У каждого потока есть собственный программный стек (с локальными переменными), счётчик команд (PC) и регистры. Память кучи (heap) является общей для всех потоков процесса.
В мобильных ОС потоки планируются вытесняющей многозадачностью (preemptive multitasking): ОС может прервать выполнение потока в любой момент и передать управление другому (context switch). Переключение контекста — дорогостоящая операция (1-10 микросекунд), так как требует сохранения/восстановления регистров CPU, обновления TLB и сброса кешей. Именно поэтому чрезмерное количество потоков (сотни и тысячи) ухудшает производительность — ОС тратит больше времени на переключение, чем на выполнение.
Поток и процесс — разные понятия. Процесс — это экземпляр приложения с выделенной виртуальной памятью. Поток внутри процесса разделяет эту память с другими потоками. В Android каждый компонент приложения (Activity, Service, BroadcastReceiver) работает в одном процессе, но может выполняться в разных потоках. iOS-приложение также является одним процессом с возможностью создания дополнительных потоков через GCD или Thread.
Каждый поток в Java/Kotlin (Android) и NSThread (iOS) проходит через пять состояний: New (создан), Runnable (готов к выполнению), Running (выполняется на CPU), Blocked/Waiting (ожидает ресурса или уведомления), Terminated (завершён). Переходы между состояниями управляются планировщиком ОС и синхронизационными примитивами. Разработчик может влиять на приоритет потока (Thread.setPriority()) и его состояние (sleep, join, interrupt).
В Android поток переходит в состояние Blocked при попытке захвата занятого монитора (synchronized), вызове Object.wait() или Thread.sleep(). В 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 | Планировщик ОС |
| Blocked/Waiting | Поток ожидает ресурс, монитор или уведомление | synchronized, wait(), sleep() |
| Terminated | Поток завершил run() или был прерван | run() завершён, interrupt() |
Context switch (переключение контекста) — операция, при которой ОС сохраняет состояние текущего потока (регистры, PC, TLB) и загружает сохранённое состояние другого. В мобильных системах (Linux + ART, XNU для iOS) context switch занимает 1-10 микросекунд. Если поток выполняет задачу за 100 микросекунд, а context switch занимает 5, то 5% времени тратится впустую. Для минимизации context switch iOS использует GCD с work stealing, Android — пулы с fixedThreadCount.
Android прошёл эволюцию от низкоуровневого java.lang.Thread до современных корутин. Каждый уровень абстракции даёт больше возможностей с меньшими накладными расходами. Thread — базовый класс, но его прямое создание не рекомендуется: новый поток не управляется пулом, его трудно мониторить и отменять. AsyncTask (deprecated с API 30) был шагом вперёд, но страдал от утечек памяти и неудобной обработки конфигураций.
HandlerThread — специальный подкласс Thread с Looper, который может обрабатывать очередь сообщений. Используется для последовательного выполнения задач на фоновом потоке, например, запись данных в 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. Прямое создание Thread — самый низкоуровневый и неэффективный подход. HandlerThread полезен для последовательных задач на фоне. Executors.newFixedThreadPool(4) создаёт пул из 4 потоков для параллельного выполнения до 10 задач. Kotlin Coroutines с Dispatchers.Default — современный, эффективный и безопасный способ.
HandlerThread — специализированный подкласс Thread с встроенным Looper и очередью сообщений. Он создаётся вызовом start(), после чего через Handler(handlerThread.looper) можно отправлять Runnable и сообщения. HandlerThread выполняет задачи строго последовательно — следующая задача не начнётся до завершения предыдущей. Это удобно для записи данных в Room или файлы, где порядок операций критичен. Вызов quitSafely() останавливает Looper после завершения текущей задачи.
iOS также предоставляет три уровня работы с потоками. Thread (Thread в Swift, NSThread в Objective-C) — низкоуровневый API, напрямую создающий нативный поток. GCD (Grand Central Dispatch) через DispatchQueue — основной инструмент для iOS-разработчиков, автоматически управляющий пулом потоков. OperationQueue — высокоуровневая абстракция над GCD с поддержкой зависимостей, приоритетов и отмены.
Прямое использование Thread в современной iOS-разработке встречается крайне редко — GCD предоставляет все необходимые возможности с автоматическим управлением памятью и потоками. Thread используется только для специфических случаев: установка thread-local storage (threadDictionary), создание RunLoop для фонового потока или интеграция с C-библиотеками, ожидающими pthread_t.
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")
}
// Barrier для синхронизации записи
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)
}
}
// Thread-safe коллекция через GCD barrier
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 демонстрирует паттерн Concurrent Read / Exclusive Write через GCD barrier. Чтение через queue.sync{} выполняется параллельно из нескольких потоков. Запись через queue.async(flags: .barrier) блокирует все другие операции (и чтение, и запись) до завершения записи. Это эффективнее, чем synchronized-блоки, так как не блокирует читатели, пока нет записи.
Прямое использование Thread в iOS оправдано в трёх случаях: для thread-local storage (Thread.current.threadDictionary) — хранения данных, привязанных к потоку; для создания специального RunLoop на фоновом потоке с performSelector:onThread:; для интеграции с C/C++ библиотеками, которые ожидают pthread_t. Во всех остальных случаях GCD через DispatchQueue предпочтительнее — он автоматически управляет пулом потоков и энергопотреблением.
Race condition (состояние гонки) возникает, когда два и более потока одновременно обращаются к общим данным, и хотя бы один из потоков выполняет запись. Результат зависит от порядка выполнения (timing) и непредсказуем. Для предотвращения race condition используются примитивы синхронизации. В мобильной разработке доступны блокировки (synchronized, NSLock), атомарные операции (AtomicInteger, atomic-свойства iOS) и очереди (serial queue).
Выбор примитива зависит от сценария. Для простых счётчиков и флагов достаточно атомарных операций (AtomicInteger, atomic property). Для критических секций с несколькими операциями — блокировки (synchronized, NSLock). Для сложных структур данных — serial DispatchQueue или GCD barrier. Блокировки проще понять, но они подвержены deadlock-ам и livelock-ам. Очереди сложнее, но безопаснее.
// Синхронизация в 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() {
// Только один поток за раз
doWork()
}
// 3. Mutex из корутин — suspend-safe
private val mutex = Mutex()
suspend fun mutexOperation() {
mutex.withLock {
// protected code — потокобезопасно
doWork()
}
}
private fun doWork() { /* critical section */ }
}
// Deadlock пример: 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 демонстрирует три подхода к синхронизации. AtomicInteger.incrementAndGet() — атомарная операция без блокировок (CAS). @Synchronized — встроенный монитор Java, блокирует весь объект. Mutex.withLock — корутинный мьютекс, приостанавливает корутину вместо блокировки потока (эффективнее). DeadlockExample показывает классический deadlock: два потока захватывают блокировки в разном порядке.
Thread Pool (пул потоков) — это набор заранее созданных потоков, которые переиспользуются для выполнения задач. Вместо создания нового потока для каждой задачи (дорого) пул берёт свободный поток из пула. Если свободных потоков нет, задача ставится в очередь. Пул автоматически управляет размером: новые потоки создаются при пиковых нагрузках, простаивающие потоки завершаются. Это снижает overhead на создание потоков в десятки раз.
В Android Executors.newFixedThreadPool(4) создаёт пул из 4 потоков. Если одновременно приходит 10 задач, 4 начнут выполняться сразу, 6 будут ждать в очереди. Executors.newCachedThreadPool() создаёт потоки по мере необходимости (без лимита) и завершает простаивающие через 60 секунд. Для iOS GCD автоматически предоставляет пулы глобальных очередей, размер которых соответствует количеству ядер CPU и текущей нагрузке.
В Kotlin Coroutines пулы потоков скрыты внутри диспатчеров. Dispatchers.Default использует пул размером = количество ядер CPU (минимум 2). Dispatchers.IO — 64 потока (достаточно для сотен IO-bound задач, так как большинство будут ожидать ввода-вывода, не занимая CPU). Каждый диспатчер автоматически масштабирует пул под нагрузку, экономя энергию батареи при простое.
Часто задаваемые вопросы
Thread — базовая единица выполнения кода в приложении. Каждый процесс может иметь множество потоков, разделяющих память, но с собственным стеком. В мобильной разработке потоки используются для параллельного выполнения задач без блокировки UI. Android использует Thread, Executors, HandlerThread и Coroutines. iOS использует Thread, GCD (DispatchQueue) и OperationQueue.
Создание Thread требует выделения ~1 МБ под стек в Android и ~512 КБ в iOS — это дорогостоящая операция. Для 1000 задач прямое создание 1000 потоков потребует ~1 ГБ только на стеки плюс накладные расходы на context switch. Вместо Thread используйте пулы (Executors, GCD) или корутины — они переиспользуют потоки, снижая overhead в десятки раз.
Race condition — непредсказуемое поведение при одновременном доступе нескольких потоков к общим данным с записью. Избежать можно тремя способами: использовать атомарные типы (AtomicInteger), блокировки (synchronized, NSLock) или сериализовать доступ через очередь (DispatchQueue serial, Actor в Kotlin). Лучшая практика — минимизировать общее mutable-состояние и использовать immutability.
Thread — нативный системный объект, занимающий ~1 МБ стека и привязанный к ядру ОС. Корутина — легковесная единица выполнения Kotlin, которая не привязана к конкретному потоку и может приостанавливаться (suspend) без блокировки. Один поток может выполнять тысячи корутин. Корутины эффективнее по памяти и позволяют писать асинхронный код без callbacks.
Deadlock проявляется как полное зависание приложения без ANR. В Android используйте Thread.getAllStackTraces() для дампа стеков всех потоков — два потока будут ждать блокировки друг друга. В iOS — Thread.callStackSymbols. Инструменты: Android Studio Profiler (Threads tab), Instruments (iOS, Thread State View). Предотвращение: захватывайте блокировки в фиксированном порядке, используйте tryLock с таймаутом.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также