Thread в мобильной разработке — что это, виды и управление потоками

Автор: IT Sectr Опубликовано: 2026-03-16 Время чтения: 11 мин

Thread — базовая единица процессорного времени, имеющая собственный стек и выполняющаяся независимо от других потоков. В мобильной разработке потоки используются для параллельного выполнения задач, чтобы интерфейс оставался отзывчивым во время длительных операций. Android поддерживает java.lang.Thread, Executors и Kotlin Coroutines, iOS — Thread (Objective-C), GCD и OperationQueue. По данным Android Thread Documentation, создание нативного потока требует выделения ~1 МБ под стек операционной системой.

Главное

  • Thread — минимальная единица планирования CPU: каждый поток независим и имеет свой стек
  • Создание потока требует ~1 МБ на стек в Android и 512 КБ в iOS, поэтому пулы эффективнее прямого создания
  • Android: Thread, Executors, HandlerThread, Coroutines — четыре уровня абстракции потоков
  • iOS: Thread (низкоуровневый), GCD (DispatchQueue), OperationQueue (высокоуровневый)
  • Thread safety — общий доступ к mutable-данным требует синхронизации: locks, atomic, serial queues

Что такое Thread

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Поток готов к выполнению, ожидает CPUthread.start()
RunningПоток выполняется на ядре CPUПланировщик ОС
Blocked/WaitingПоток ожидает ресурс, монитор или уведомлениеsynchronized, wait(), sleep()
TerminatedПоток завершил run() или был прерванrun() завершён, interrupt()

Context Switch и его стоимость

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.

Thread в Android: от Thread до Coroutines

Android прошёл эволюцию от низкоуровневого java.lang.Thread до современных корутин. Каждый уровень абстракции даёт больше возможностей с меньшими накладными расходами. Thread — базовый класс, но его прямое создание не рекомендуется: новый поток не управляется пулом, его трудно мониторить и отменять. AsyncTask (deprecated с API 30) был шагом вперёд, но страдал от утечек памяти и неудобной обработки конфигураций.

HandlerThread — специальный подкласс Thread с Looper, который может обрабатывать очередь сообщений. Используется для последовательного выполнения задач на фоновом потоке, например, запись данных в 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. Прямое создание Thread — самый низкоуровневый и неэффективный подход. HandlerThread полезен для последовательных задач на фоне. Executors.newFixedThreadPool(4) создаёт пул из 4 потоков для параллельного выполнения до 10 задач. Kotlin Coroutines с Dispatchers.Default — современный, эффективный и безопасный способ.

HandlerThread: последовательные фоновые задачи

HandlerThread — специализированный подкласс Thread с встроенным Looper и очередью сообщений. Он создаётся вызовом start(), после чего через Handler(handlerThread.looper) можно отправлять Runnable и сообщения. HandlerThread выполняет задачи строго последовательно — следующая задача не начнётся до завершения предыдущей. Это удобно для записи данных в Room или файлы, где порядок операций критичен. Вызов quitSafely() останавливает Looper после завершения текущей задачи.

Thread в iOS: Thread, GCD и OperationQueue

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.

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")
        }

        // 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-блоки, так как не блокирует читатели, пока нет записи.

iOS Thread vs GCD: когда использовать Thread напрямую

Прямое использование Thread в iOS оправдано в трёх случаях: для thread-local storage (Thread.current.threadDictionary) — хранения данных, привязанных к потоку; для создания специального RunLoop на фоновом потоке с performSelector:onThread:; для интеграции с C/C++ библиотеками, которые ожидают pthread_t. Во всех остальных случаях GCD через DispatchQueue предпочтительнее — он автоматически управляет пулом потоков и энергопотреблением.

Синхронизация потоков: locks, atomic, serial queues

Race condition (состояние гонки) возникает, когда два и более потока одновременно обращаются к общим данным, и хотя бы один из потоков выполняет запись. Результат зависит от порядка выполнения (timing) и непредсказуем. Для предотвращения race condition используются примитивы синхронизации. В мобильной разработке доступны блокировки (synchronized, NSLock), атомарные операции (AtomicInteger, atomic-свойства iOS) и очереди (serial queue).

Выбор примитива зависит от сценария. Для простых счётчиков и флагов достаточно атомарных операций (AtomicInteger, atomic property). Для критических секций с несколькими операциями — блокировки (synchronized, NSLock). Для сложных структур данных — serial DispatchQueue или GCD barrier. Блокировки проще понять, но они подвержены deadlock-ам и livelock-ам. Очереди сложнее, но безопаснее.

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() {
        // Только один поток за раз
        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 Pools: почему Executors лучше Thread

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 в мобильной разработке?

Thread — базовая единица выполнения кода в приложении. Каждый процесс может иметь множество потоков, разделяющих память, но с собственным стеком. В мобильной разработке потоки используются для параллельного выполнения задач без блокировки UI. Android использует Thread, Executors, HandlerThread и Coroutines. iOS использует Thread, GCD (DispatchQueue) и OperationQueue.

Почему не рекомендуется создавать Thread напрямую?

Создание Thread требует выделения ~1 МБ под стек в Android и ~512 КБ в iOS — это дорогостоящая операция. Для 1000 задач прямое создание 1000 потоков потребует ~1 ГБ только на стеки плюс накладные расходы на context switch. Вместо Thread используйте пулы (Executors, GCD) или корутины — они переиспользуют потоки, снижая overhead в десятки раз.

Что такое race condition и как её избежать?

Race condition — непредсказуемое поведение при одновременном доступе нескольких потоков к общим данным с записью. Избежать можно тремя способами: использовать атомарные типы (AtomicInteger), блокировки (synchronized, NSLock) или сериализовать доступ через очередь (DispatchQueue serial, Actor в Kotlin). Лучшая практика — минимизировать общее mutable-состояние и использовать immutability.

Чем отличается Thread от корутины?

Thread — нативный системный объект, занимающий ~1 МБ стека и привязанный к ядру ОС. Корутина — легковесная единица выполнения Kotlin, которая не привязана к конкретному потоку и может приостанавливаться (suspend) без блокировки. Один поток может выполнять тысячи корутин. Корутины эффективнее по памяти и позволяют писать асинхронный код без callbacks.

Как отловить deadlock в мобильном приложении?

Deadlock проявляется как полное зависание приложения без ANR. В Android используйте Thread.getAllStackTraces() для дампа стеков всех потоков — два потока будут ждать блокировки друг друга. В iOS — Thread.callStackSymbols. Инструменты: Android Studio Profiler (Threads tab), Instruments (iOS, Thread State View). Предотвращение: захватывайте блокировки в фиксированном порядке, используйте tryLock с таймаутом.

Итоги

  • Thread — минимальная единица CPU: независимое выполнение с собственным стеком, память кучи общая
  • Пять состояний потока: New, Runnable, Running, Blocked/Waiting, Terminated
  • Android эволюционировал от Thread → AsyncTask → Executors → HandlerThread → Coroutines
  • iOS предоставляет Thread, GCD (DispatchQueue) и OperationQueue — от низкого к высокому уровню
  • Race condition решается блокировками (synchronized, NSLock), атомарными типами и serial-очередями
  • Deadlock возникает при перекрёстном захвате блокировок — предотвращается фиксированным порядком
  • Thread Pool эффективнее создания новых Thread: переиспользует потоки, снижает overhead на context switch

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также