Работа с очередями задач в Kotlin: на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: зачем нужны очереди задач в Kotlin?

Современные приложения всё чаще требуют параллельной обработки данных, фоновых операций и асинхронных вызовов. Без грамотного управления задачами ваш код может столкнуться с проблемами производительности, утечками памяти или «зависаниями» интерфейса. Очереди задач в Kotlin — это не просто абстракция, а мощный инструмент, который помогает организовать выполнение работы в нужном порядке, с контролем количества потоков и приоритетов.

В этой статье мы разберём практические подходы к реализации очередей задач на Kotlin: от классического ThreadPoolExecutor до корутин с их гибкими диспетчерами. Вы увидите, как применить эти техники в реальных проектах, и получите готовые примеры кода.

1. Классический подход: ThreadPoolExecutor и BlockingQueue

Стандартная библиотека Java (и Kotlin, как JVM-язык) предоставляет мощный класс ThreadPoolExecutor. Он управляет пулом потоков и использует внутреннюю блокирующую очередь для хранения ожидающих задач.

Создание очереди с фиксированным числом потоков

Наиболее простой случай — пул с фиксированным количеством потоков. Все задачи, которые не могут быть выполнены немедленно, помещаются в очередь LinkedBlockingQueue.

import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

fun main() {
    val executor = Executors.newFixedThreadPool(3)

    for (i in 1..10) {
        executor.submit {
            println("Задача $i выполняется в потоке ${Thread.currentThread().name}")
            Thread.sleep(1000)
        }
    }

    executor.shutdown()
    executor.awaitTermination(1, TimeUnit.MINUTES)
    println("Все задачи завершены")
}

Этот код демонстрирует базовую очередь: задачи отправляются в пул, и если все потоки заняты, они ждут в очереди.

Приоритетная очередь: PriorityBlockingQueue

Иногда требуется выполнять срочные задачи раньше остальных. Для этого используем PriorityBlockingQueue с кастомным компаратором.

import java.util.concurrent.PriorityBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit

data class PriorityTask(val priority: Int, val name: String) : Comparable<PriorityTask> {
    override fun compareTo(other: PriorityTask): Int = other.priority - this.priority
}

fun main() {
    val queue = PriorityBlockingQueue<Runnable>(10) { r1, r2 ->
        val t1 = (r1 as PriorityTaskWrapper).task
        val t2 = (r2 as PriorityTaskWrapper).task
        t1.compareTo(t2)
    }

    val executor = ThreadPoolExecutor(2, 4, 1, TimeUnit.SECONDS, queue)

    class PriorityTaskWrapper(val task: PriorityTask, val action: () -> Unit) : Runnable {
        override fun run() = action()
    }

    for (i in 1..5) {
        val priority = if (i % 2 == 0) 10 else 1
        executor.submit(PriorityTaskWrapper(PriorityTask(priority, "Задача $i")) {
            println("Выполняется: ${PriorityTask(priority, "Задача $i")}")
            Thread.sleep(500)
        })
    }

    executor.shutdown()
    executor.awaitTermination(1, TimeUnit.MINUTES)
}

Здесь задачи с priority = 10 будут выполняться раньше, чем с priority = 1, даже если они добавлены позже.

2. Очереди задач с помощью корутин (kotlinx.coroutines)

Современный Kotlin предлагает корутины — лёгковесные потоки, которые позволяют строить асинхронные очереди без прямого управления потоками. Основной инструмент — Channel и Dispatchers.

Простая очередь через Channel

Channel — это аналог блокирующей очереди, но для корутин. Он позволяет безопасно передавать задачи между корутинами.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

suspend fun processTask(task: String) {
    delay(1000)
    println("Обработана задача: $task в ${Thread.currentThread().name}")
}

fun main() = runBlocking {
    val channel = Channel<String>(capacity = 10)

    // Продюсер — отправляет задачи
    val producer = launch {
        for (i in 1..10) {
            channel.send("Task $i")
            println("Отправлена задача $i")
        }
        channel.close() // сигнал, что задач больше не будет
    }

    // Консюмер — выполняет задачи последовательно
    val consumer = launch(Dispatchers.Default) {
        for (task in channel) {
            processTask(task)
        }
    }

    producer.join()
    consumer.join()
}

Этот паттерн гарантирует, что задачи будут обработаны в порядке поступления, и не требует явного управления потоками.

Параллельная обработка с Dispatchers

Если вам нужно обрабатывать несколько задач одновременно, используйте Dispatchers.Default или кастомный пул через newFixedThreadPoolContext.

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel

suspend fun heavyTask(id: Int) {
    delay(2000)
    println("Тяжёлая задача $id завершена")
}

fun main() = runBlocking {
    val channel = Channel<Int>(capacity = 5)
    val dispatcher = newFixedThreadPoolContext(3, "WorkerPool")

    // Продюсер
    launch {
        for (i in 1..8) {
            channel.send(i)
        }
        channel.close()
    }

    // Несколько консюмеров
    repeat(3) { consumerId ->
        launch(dispatcher) {
            for (taskId in channel) {
                println("Consumer $consumerId взял задачу $taskId")
                heavyTask(taskId)
            }
        }
    }

    delay(10000) // ждём завершения всех задач
}

Здесь три корутины в пуле параллельно забирают задачи из общего канала, что даёт эффект очереди с фиксированным числом работников.

3. Продвинутый сценарий: очередь с приоритетом на корутинах

К сожалению, стандартный Channel не поддерживает приоритеты. Однако мы можем реализовать свою очередь на основе PriorityQueue и мьютекса.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.PriorityQueue

class PriorityTaskQueue(private val capacity: Int = Int.MAX_VALUE) {
    private val queue = PriorityQueue<Pair<Int, () -> Unit>(compareBy { it.first })
    private val mutex = Mutex()
    private var isClosed = false

    suspend fun add(priority: Int, task: () -> Unit                

Блоги

Рекомендаций по книгам