O que e Job em Kotlin?

O Job e o elemento do contexto de uma coroutine que representa seu ciclo de vida. Ele permite controlar a execucao da coroutine: verificar se esta ativa, esperar sua conclusao ou cancela-la. Todo launch retorna um Job, e todo async retorna um Deferred (que e um subtipo de Job com resultado).

Pense no Job como a “alca” que voce segura para controlar uma coroutine que esta rodando em background.

Sintaxe basica

import kotlinx.coroutines.*

fun main() = runBlocking {
    val job = launch {
        delay(2000)
        println("Coroutine finalizada")
    }

    println("Job ativo? ${job.isActive}")     // true
    println("Job completo? ${job.isCompleted}") // false

    job.join() // Espera a coroutine terminar

    println("Job ativo? ${job.isActive}")     // false
    println("Job completo? ${job.isCompleted}") // true
}

O metodo join() suspende a coroutine atual ate que o job termine. Diferente de Thread.join(), ele nao bloqueia a thread.

Estados do Job

Um Job passa por varios estados durante sua vida:

New -> Active -> Completing -> Completed
                     |
                     v
               Cancelling -> Cancelled
fun main() = runBlocking {
    val job = launch(start = CoroutineStart.LAZY) {
        println("Executando...")
        delay(1000)
        println("Finalizado")
    }

    println("Novo: isActive=${job.isActive}")         // false
    job.start()
    println("Ativo: isActive=${job.isActive}")        // true
    job.join()
    println("Completo: isCompleted=${job.isCompleted}") // true
}

Com CoroutineStart.LAZY, o job comeca no estado “New” e so entra em “Active” quando voce chama start() ou join().

Cancelamento de Jobs

O cancelamento e cooperativo em Kotlin. Chamar cancel() nao mata a coroutine instantaneamente – ela precisa verificar o cancelamento:

fun main() = runBlocking {
    val job = launch {
        repeat(1000) { i ->
            println("Trabalhando... $i")
            delay(100) // Ponto de suspensao: verifica cancelamento
        }
    }

    delay(500)
    println("Cancelando...")
    job.cancel()
    job.join() // Espera a coroutine finalizar o cancelamento
    // Ou: job.cancelAndJoin() // Combina cancel() + join()
    println("Cancelado")
}

Funcoes como delay, yield e withContext verificam o cancelamento automaticamente. Se sua coroutine faz trabalho intensivo de CPU sem pontos de suspensao, use isActive ou ensureActive():

val job = launch(Dispatchers.Default) {
    var i = 0
    while (isActive) { // Verifica cancelamento manualmente
        i++
        // Trabalho intensivo de CPU
    }
    println("Parou em $i")
}

Hierarquia de Jobs (Structured Concurrency)

Jobs formam uma hierarquia pai-filho. Quando uma coroutine lanca outra, o job filho e registrado no job pai:

fun main() = runBlocking {
    val jobPai = launch {
        val jobFilho1 = launch {
            delay(2000)
            println("Filho 1 finalizado")
        }
        val jobFilho2 = launch {
            delay(1000)
            println("Filho 2 finalizado")
        }
        println("Filhos lancados")
    }

    delay(500)
    jobPai.cancel() // Cancela pai E todos os filhos
    jobPai.join()
    println("Tudo cancelado")
}

Regras da hierarquia:

  • Cancelar o pai cancela todos os filhos.
  • Uma excecao nao tratada em um filho cancela o pai e todos os irmaos.
  • O pai so e considerado completo quando todos os filhos terminam.

Job vs Deferred

Deferred<T> e um Job que retorna um resultado:

fun main() = runBlocking {
    val deferred: Deferred<Int> = async {
        delay(1000)
        42
    }

    // Deferred tem todas as funcionalidades de Job
    println("Ativo: ${deferred.isActive}")

    // Mais: pode obter o resultado
    val resultado = deferred.await()
    println("Resultado: $resultado")
}

Use launch (Job) quando nao precisa de resultado; use async (Deferred) quando precisa.

SupervisorJob

O SupervisorJob modifica o comportamento de propagacao de erros: a falha de um filho nao cancela os irmaos:

fun main() = runBlocking {
    val supervisor = SupervisorJob()

    val scope = CoroutineScope(coroutineContext + supervisor)

    val job1 = scope.launch {
        delay(1000)
        throw RuntimeException("Erro no job 1")
    }

    val job2 = scope.launch {
        delay(2000)
        println("Job 2 finalizado normalmente") // Executa mesmo com erro no job1
    }

    delay(3000)
    supervisor.cancel()
}

SupervisorJob e essencial em cenarios onde tarefas sao independentes e a falha de uma nao deve afetar as outras.

Completable Job

CompletableJob e um Job que pode ser completado manualmente:

fun main() = runBlocking {
    val job = Job() // Cria um CompletableJob

    launch(job) {
        delay(1000)
        println("Tarefa executada")
    }

    job.complete() // Marca como completando (nao aceita mais filhos)
    job.join()     // Espera filhos existentes terminarem
    println("Tudo feito")
}

Quando usar Job diretamente

  • Cancelamento controlado: quando voce precisa cancelar uma operacao especifica em resposta a uma acao do usuario.
  • Esperar conclusao: quando uma parte do codigo depende de outra coroutine terminar antes de continuar.
  • Monitorar estado: quando voce precisa saber se uma coroutine ainda esta rodando.
  • Escopo customizado: criar CoroutineScopes com Jobs especificos para controlar grupos de coroutines.

Erros comuns

  1. Nao chamar join apos cancel: cancel() apenas sinaliza o cancelamento. Sem join(), a coroutine pode continuar executando por um tempo. Use cancelAndJoin().

  2. Ignorar a cooperatividade do cancelamento: loops de CPU sem verificacao de isActive ou pontos de suspensao nao serao cancelados.

  3. Usar Job() como pai sem entender a propagacao: um Job criado manualmente com Job() segue as regras normais de propagacao de erro. Para isolamento, use SupervisorJob().

  4. Esquecer o tratamento de CancellationException: quando um job e cancelado, CancellationException e lancada. Ela deve ser propagada, nao capturada em catch generico.

  5. Nao estruturar coroutines: lancar coroutines com GlobalScope em vez de usar escopos estruturados perde todo o beneficio de hierarquia de Jobs.

Termos relacionados

  • Coroutine: a unidade de execucao cujo ciclo de vida o Job representa.
  • Deferred: subtipo de Job que carrega um resultado, retornado por async.
  • SupervisorJob: job especial onde falha de filhos nao propaga para irmaos.
  • CoroutineScope: escopo que contem um Job e define o ciclo de vida das coroutines.
  • Dispatcher: trabalha junto com o Job no contexto da coroutine, determinando onde ela executa.
  • Structured Concurrency: o principio de que coroutines formam hierarquias atraves de Jobs.

O Job e a peca central do controle de ciclo de vida em Kotlin Coroutines. Dominar Jobs, cancelamento e hierarquia e fundamental para escrever codigo concorrente que seja robusto, previsivel e livre de vazamentos de recursos.