Kotlin: Coroutines do Kotlin Explicadas
Última atualização: 2026-08-26
Coroutines são o núcleo da programação assíncrona do Kotlin — a função suspend permite que Charlie escreva operações assíncronas com sintaxe síncrona, enquanto a concorrência estruturada garante nenhuma coroutine vazada.
1. O que Você Aprenderá
- Coroutines vs threads: leves, não-bloqueantes, concorrência estruturada
- Funções
suspend: "sintaxe síncrona" no mundo assíncrono CoroutineScope/Job/Dispatcher: os três pilares da concorrência estruturadalaunch(dispara-e-esquece) vsasync(aguarda resultado)- Charlie em ação:
OrderProcessorconcorrente comasync+await
2. A História Real de um Arquiteto
(1) Ponto de Dor: Inferno de Callbacks e Explosão de Threads
A versão Java do OrderProcessor de Charlie usava CompletableFuture para lógica assíncrona — 3 níveis de callbacks aninhados já eram ilegíveis. A equipe usava 200 threads para requisições concorrentes, com sobrecarga de troca de contexto de CPU consumindo 40% da capacidade.
(2) A Solução com Coroutines
// Java: callbacks aninhados de CompletableFuture
CompletableFuture<Order> future = fetchOrder(id)
.thenCompose(order -> fetchCustomer(order.getCustomerId()))
.thenApply(customer -> enrichOrder(order, customer))
.exceptionally(ex -> handleError(ex));
// Kotlin: código com aparência sequencial, execução assíncrona
suspend fun processOrder(id: String): Order {
val order = fetchOrder(id) // Suspende, não bloqueia
val customer = fetchCustomer(order.customerId) // Suspende novamente
return enrichOrder(order, customer)
}
Funções
suspendfazem código assíncrono ler como síncrono — coroutines suspendem em vez de bloquear threads ao esperar. Uma thread pode lidar com 100.000 coroutines.
3. Coroutines vs Threads
(1) Diferenças Principais
// Thread: 1 thread por tarefa concorrente (pesada)
// 100.000 threads = OOM (cada thread ~1MB de stack)
// Coroutine: threads virtuais leves
// 100.000 coroutines = tranquilo (cada coroutine ~poucas centenas de bytes)
fun main() = runBlocking {
repeat(100_000) {
launch { // 100K coroutines - sem problema!
delay(1_000) // Suspende (não bloqueia)
println("Coroutine $it concluída")
}
}
}
(2) Comparação Coroutine vs Thread
| Dimensão | Thread | Coroutine |
|---|---|---|
| Custo de criação | ~1MB de memória stack | ~poucas centenas de bytes |
| Troca de contexto | Nível de kernel do SO (lenta) | Modo usuário (rápida) |
| Quantidade máxima | Milhares | Centenas de milhares |
| Bloqueio | Bloqueia toda a thread | Apenas suspende a coroutine |
| Cancelamento | Inseguro (stop deprecado) | Cancelamento cooperativo (seguro) |
| Exceções | Difícil de propagar | Propagação estruturada |
(3) Diagrama de Princípio da Coroutine
sequenceDiagram
participant T as Thread
participant C1 as Coroutine 1
participant C2 as Coroutine 2
participant IO as IO Operation
T->>C1: Resume
C1->>IO: fetchOrder(id)
Note over C1: SUSPEND - thread está LIVRE
T->>C2: Resume (mesma thread!)
C2->>IO: fetchCustomer(id)
Note over C2: SUSPEND - thread está LIVRE
IO-->>C1: Resultado do Order
Note over C1: RESUME
C1-->>T: Continuar processamento
IO-->>C2: Resultado do Customer
Note over C2: RESUME
4. Funções suspend
(1) Conceitos Básicos
// palavra-chave suspend: esta função pode suspender a coroutine
suspend fun fetchOrder(id: String): Order {
delay(500) // Simular chamada de rede (não-bloqueante)
return Order(id, 299.99, "CONFIRMED")
}
// funções suspend só podem ser chamadas de coroutines ou outras funções suspend
suspend fun processOrder(id: String): Order {
val order = fetchOrder(id) // Ponto de suspensão
val customer = fetchCustomer(order.customerId) // Ponto de suspensão
return order.copy(customer = customer)
}
(2) Regras de Funções suspend
| Regra | Descrição |
|---|---|
| Apenas chamável de coroutines ou funções suspend | Imposto pelo compilador |
| Não bloqueia threads | Suspende a coroutine, libera a thread |
| Pode chamar funções regulares | Funções regulares não podem chamar suspend |
| Essencialmente CPS transformado | O compilador converte suspend em uma máquina de estados |
5. Os Três Pilares da Concorrência Estruturada
(1) CoroutineScope
// CoroutineScope: define o tempo de vida das coroutines
// Todas as coroutines lançadas em um escopo estão ligadas ao seu tempo de vida
// runBlocking: bloqueia a thread atual até todas as coroutines completarem
runBlocking {
launch { delay(1_000); println("Concluído") }
}
// coroutineScope: suspende (não bloqueia) até todos os filhos completarem
suspend fun fetchAll() = coroutineScope {
val order = async { fetchOrder("ORD-001") }
val customer = async { fetchCustomer("CUST-001") }
Pair(order.await(), customer.await())
}
// Escopo personalizado (ex.: em uma classe)
class OrderService {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
fun process(order: Order) {
scope.launch { /* trabalho assíncrono */ }
}
fun shutdown() {
scope.cancel() // Cancelar todas as coroutines filhas
}
}
(2) Job — Ciclo de Vida da Coroutine
val job = launch {
println("Trabalhando...")
delay(1_000)
println("Concluído")
}
// Estados do Job: New -> Active -> Completing -> Completed
// -> Cancelling -> Cancelled
job.cancel() // Solicitar cancelamento
job.join() // Aguardar conclusão
job.cancelAndJoin() // Cancelar + aguardar
(3) Dispatcher — Agendador de Threads
// Dispatchers.Default: trabalho intensivo de CPU (paralelismo = núcleos de CPU)
launch(Dispatchers.Default) { computeOrderTax() }
// Dispatchers.IO: operações de I/O bloqueantes (até 64 threads)
launch(Dispatchers.IO) { fetchDataFromDb() }
// Dispatchers.Main: thread de UI (Android/Swing)
launch(Dispatchers.Main) { updateUI() }
// Dispatcher personalizado
val orderDispatcher = Executors.newFixedThreadPool(8).asCoroutineDispatcher()
launch(orderDispatcher) { processOrder() }
(4) Comparação de Dispatchers
| Dispatcher | Quantidade de Threads | Caso de Uso | Operações Típicas |
|---|---|---|---|
Default |
Núcleos de CPU | Intensivo de CPU | Ordenação, computação |
IO |
Até 64 | I/O bloqueante | Rede, banco de dados |
Main |
1 | Atualizações de UI | Android/Desktop |
| Personalizado | Personalizado | Necessidades específicas | Pool de threads isolado |
6. launch vs async
(1) launch — Dispara-e-Esquece
// launch: dispara-e-esquece (retorna Job, não resultado)
val job: Job = launch {
delay(1_000)
println("Trabalho em background concluído")
}
job.join() // Aguardar conclusão
(2) async — Aguarda Resultado
// async: retorna Deferred<T> (um Job tipo future)
val deferred: Deferred<Order> = async {
fetchOrder("ORD-001")
}
val order = deferred.await() // Suspende até o resultado estar pronto
(3) Composição Concorrente
suspend fun processOrderConcurrently(id: String): EnrichedOrder = coroutineScope {
// Lançar em paralelo dentro de coroutineScope
val orderDeferred = async { fetchOrder(id) }
val customerDeferred = async { fetchCustomer(id) }
val inventoryDeferred = async { checkInventory(id) }
// Aguardar todos os resultados
val order = orderDeferred.await()
val customer = customerDeferred.await()
val inventory = inventoryDeferred.await()
EnrichedOrder(order, customer, inventory)
}
(4) Comparação launch vs async
| Dimensão | launch |
async |
|---|---|---|
| Valor de retorno | Job |
Deferred<T> |
| Obter resultado | N/A | .await() |
| Tratamento de exceções | Propaga para o pai | Armazenado no Deferred |
| Caso de uso | Efeitos colaterais (logging, notificações) | Precisa de valor de retorno |
| Analogia | Thread.start() |
CompletableFuture |
7. Tratamento de Exceções em Coroutines
// try-catch em coroutine
launch {
try {
fetchOrder(id)
} catch (e: Exception) {
logger.error("Falha ao buscar pedido", e)
}
}
// CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
logger.error("Erro na coroutine", exception)
}
launch(handler) {
fetchOrder(id) // Exceção não capturada tratada pelo handler
}
// SupervisorJob: falha de um filho não cancela os irmãos
coroutineScope {
val supervisor = SupervisorJob()
with(supervisor) {
launch { throw Exception("Filho 1 falha") } // Apenas este filho falha
launch { delay(100); println("Filho 2 ainda executa") } // Irmão sobrevive
}
}
8. Exemplo Completo: OrderProcessor Processamento Assíncrono
// ============================================
// OrderProcessor - Processamento Assíncrono com Coroutines
// Recurso: Enriquecimento concorrente de pedidos
// ============================================
import kotlinx.coroutines.*
data class Order(val id: String, val total: Double, val customerId: String, var customerName: String? = null)
data class Customer(val id: String, val name: String, val tier: String)
// Simular operações assíncronas
suspend fun fetchOrder(id: String): Order {
delay(100) // Simular consulta ao banco
return Order(id, 299.99 + id.substring(4).toInt() * 100, "CUST-${id.substring(4)}")
}
suspend fun fetchCustomer(id: String): Customer {
delay(150) // Simular chamada de API
return Customer(id, "Customer-$id", if (id.endsWith("1")) "VIP" else "STANDARD")
}
suspend fun checkInventory(orderId: String): Boolean {
delay(80) // Simular verificação de inventário
return true
}
// Processamento sequencial
suspend fun processSequential(id: String): Order {
val order = fetchOrder(id) // 100ms
val customer = fetchCustomer(order.customerId) // 150ms
val available = checkInventory(id) // 80ms
// Total: ~330ms
return order.copy(customerName = customer.name)
}
// Processamento concorrente
suspend fun processConcurrent(id: String): Order = coroutineScope {
val orderDeferred = async { fetchOrder(id) }
val order = orderDeferred.await()
// Buscar cliente e inventário em paralelo
val customerDeferred = async { fetchCustomer(order.customerId) }
val inventoryDeferred = async { checkInventory(id) }
val customer = customerDeferred.await()
val available = inventoryDeferred.await()
// Total: ~100ms + ~150ms = ~250ms (cliente e inventário em paralelo)
if (!available) throw RuntimeException("Inventário indisponível para $id")
order.copy(customerName = "${customer.name} (${customer.tier})")
}
// Processamento em lote
suspend fun processBatch(orderIds: List<String>): List<Order> = coroutineScope {
orderIds.map { id ->
async { processConcurrent(id) }
}.awaitAll()
}
fun main() = runBlocking {
val orderIds = listOf("ORD-001", "ORD-002", "ORD-003", "ORD-004", "ORD-005")
// Medir sequencial
val seqStart = System.currentTimeMillis()
val seqResults = orderIds.map { processSequential(it) }
val seqTime = System.currentTimeMillis() - seqStart
println("Sequencial: ${seqTime}ms para ${seqResults.size} pedidos")
// Medir concorrente
val conStart = System.currentTimeMillis()
val conResults = processBatch(orderIds)
val conTime = System.currentTimeMillis() - conStart
println("Concorrente: ${conTime}ms para ${conResults.size} pedidos")
// Imprimir resultados
println("\n=== Pedidos Processados ===")
conResults.forEach { order ->
println(" ${order.id}: \$${order.total} USD | ${order.customerName}")
}
// Concorrência estruturada: erro em um cancela todos
println("\n=== Tratamento de Erros ===")
try {
coroutineScope {
launch { delay(200); println("Tarefa 1 concluída") }
launch { delay(100); throw RuntimeException("Tarefa 2 falhou!") }
launch { delay(300); println("Tarefa 3 concluída") }
}
} catch (e: RuntimeException) {
println("Capturado: ${e.message}")
}
}
Saída:
Sequencial: 1650ms para 5 pedidos
Concorrente: 370ms para 5 pedidos
=== Pedidos Processados ===
ORD-001: $1100 USD | Customer-CUST-001 (VIP)
ORD-002: $1200 USD | Customer-CUST-002 (STANDARD)
ORD-003: $1300 USD | Customer-CUST-003 (STANDARD)
ORD-004: $1400 USD | Customer-CUST-004 (STANDARD)
ORD-005: $1500 USD | Customer-CUST-005 (STANDARD)
=== Tratamento de Erros ===
Tarefa 1 concluída
Capturado: Tarefa 2 falhou!
9. Exemplos práticos rápidos
▶ Exemplo: launch e runBlocking
import kotlinx.coroutines.*
// runBlocking: thread principal espera corrotinas terminarem
fun main() = runBlocking {
println("Início do main")
// launch: retorna Job, não bloqueia
val job = launch {
delay(500)
println("Lançado após 500ms")
}
println("Continuando no main")
job.join()
println("Lançado concluído")
}
// Saída:
// Início do main
// Continuando no main
// Lançado após 500ms
// Lançado concluído
▶ Exemplo: async e await
import kotlinx.coroutines.*
fun main() = runBlocking {
// async retorna Deferred<T> (futuro)
val time = measureTimeMillis {
val r1 = async { fetchData("ORD-001") }
val r2 = async { fetchData("ORD-002") }
val r3 = async { fetchData("ORD-003") }
// await suspende até obter o resultado
val results = listOf(r1.await(), r2.await(), r3.await())
println("Resultados: $results")
}
println("Tempo total: ${time}ms (sequencial seria ~3000ms)")
}
suspend fun fetchData(id: String): String {
delay(1000)
return "Resultado($id)"
}
// Saída:
// Resultados: [Resultado(ORD-001), Resultado(ORD-002), Resultado(ORD-003)]
// Tempo total: ~1000ms
▶ Exemplo: Dispatchers e troca de contexto
import kotlinx.coroutines.*
fun main() = runBlocking {
val start = Thread.currentThread().name
// Dispatador principal (ou Main se disponível)
val result = withContext(Dispatchers.Default) {
// CPU-bound executado no pool Default
val heavyCompute = (1..1_000_000).sum()
println("Worker thread: ${Thread.currentThread().name}")
heavyCompute
}
println("Original thread: $start")
println("Resultado: $result")
}
// Saída:
// Worker thread: DefaultDispatcher-worker-1
// Original thread: main
// Resultado: 1784293664...
▶ Exemplo: job e cancelamento
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("Trabalho $i")
delay(100)
}
}
delay(250)
println("Cancelando...")
job.cancel()
job.join()
println("Cancelado")
}
// Saída:
// Trabalho 0
// Trabalho 1
// Trabalho 2
// Cancelando...
// Cancelado
▶ Exemplo: Exceções em corrotinas
import kotlinx.coroutines.*
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Capturado: ${exception.message}")
}
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)
scope.launch {
try {
delay(100)
throw RuntimeException("Falha em tarefa 1")
} catch (e: Exception) {
println("Capturado localmente: ${e.message}")
}
}
scope.launch {
delay(50)
throw RuntimeException("Falha em tarefa 2")
}
delay(1000)
scope.cancel()
}
// Saída:
// Capturado: Falha em tarefa 2
// Capturado localmente: Falha em tarefa 1
▶ Exemplo: SupervisorJob vs Job normal
import kotlinx.coroutines.*
fun main() = runBlocking {
// Sem SupervisorJob: falha de uma tarefa cancela irmãos
println("=== Job normal ===")
try {
coroutineScope {
launch { delay(100); throw RuntimeException("Erro A") }
launch { delay(200); println("Tarefa B") }
}
} catch (e: Exception) {
println("Capturado: ${e.message}")
}
// Com SupervisorJob: filhos falham independentemente
println("\n=== SupervisorJob ===")
supervisorScope {
launch { delay(100); throw RuntimeException("Erro A") }
launch { delay(200); println("Tarefa B continua") }
}
}
// Saída:
// === Job normal ===
// Capturado: Erro A
//
// === SupervisorJob ===
// Capturado: (no handler) Erro A
// Tarefa B continua
▶ Exemplo: Flow basic
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking {
// Flow é stream frio - emite valores sob demanda
val flow = flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}
// collect terminal
flow.collect { println("Valor: $it") }
// Operadores intermediários
val squared = (1..5).asFlow()
.map { it * it }
.filter { it > 5 }
.toList()
println("Quadrados > 5: $squared")
}
❓ Perguntas Frequentes
P: Qual a diferença entre coroutines e Virtual Threads? R: Coroutines são a implementação em modo usuário do Kotlin requerendo marcadores
suspend; Virtual Threads são de nível JVM (JDK 21+) sem necessidade de mudanças no código. Coroutines são mais flexíveis (multiplataforma), enquanto Virtual Threads são mais transparentes (sem mudanças de biblioteca).
P: Qual a diferença entre delay() e Thread.sleep()? R:
delay()suspende a coroutine e libera a thread;Thread.sleep()bloqueia toda a thread. Em coroutines, sempre usedelay, nuncaThread.sleep.
P: Quando devo usar runBlocking? R: Principalmente em funções
main()e testes como ponto de entrada de coroutines. Evite em código de produção — ele bloqueia a thread atual.
P: Como limpar recursos após cancelamento de coroutine? R: Use
try-finallyou funçõesuse. Execute limpeza no bloco finally quando uma coroutine é cancelada para garantir liberação de recursos.
P: Quando uma exceção async é lançada? R: A exceção de
asyncé armazenada noDeferrede lançada apenas quando.await()é chamado. Se você nunca aguardar, a exceção é silenciosamente perdida.
P: Quando devo usar SupervisorJob? R: Quando a falha de um filho não deve afetar os irmãos. Por exemplo, em código de UI, uma requisição falhando não deve cancelar as outras. Em um
coroutineScopenormal, um filho falhando cancela todos os irmãos.
📖 Resumo
- Coroutines são threads leves com custo de criação extremamente baixo (centenas de bytes vs 1MB)
- Funções
suspendfazem código assíncrono ler como síncrono — elas suspendem, não bloqueiam - Concorrência estruturada:
coroutineScopegarante que todas as coroutines filhas completam ou cancelam launchdispara e esquece;asyncaguarda um valor de retornoDispatchercontrola qual pool de threads executa: Default / IO / Main- Tratamento de exceções:
try-catch/CoroutineExceptionHandler/SupervisorJob
📝 Exercícios
- Iniciante (⭐): Use
runBlocking+launchpara iniciar 3 coroutines, cada uma atrasando um tempo diferente e imprimindo uma mensagem. Dica:launch { delay(N); println(...) } - Intermediário (⭐⭐): Use
coroutineScope+asyncpara buscar concorrentemente informações de pedido e cliente, combinando-os em um pedido completo. Dica:async { fetchOrder },await() - Avançado (⭐⭐⭐): Implemente uma função de busca de pedido com timeout e retry: timeout de 3 segundos, até 3 tentativas, backoff exponencial. Dica:
withTimeout+ loopretry