O que e Dispatcher em Kotlin?

Um Dispatcher em Kotlin Coroutines determina em qual thread ou pool de threads uma coroutine sera executada. Ele e o mecanismo que conecta o mundo das coroutines ao mundo das threads do sistema operacional, decidindo onde o codigo realmente vai rodar.

Escolher o dispatcher correto e fundamental para a performance e o comportamento da aplicacao. Uma operacao de rede executada no dispatcher errado pode travar a interface do usuario; um calculo pesado no dispatcher errado pode desperdicar recursos.

Os dispatchers principais

O Kotlin fornece quatro dispatchers padroes atraves do objeto Dispatchers:

import kotlinx.coroutines.*

fun main() = runBlocking {
    // Dispatcher padrao para trabalho intensivo de CPU
    launch(Dispatchers.Default) {
        println("Default: ${Thread.currentThread().name}")
    }

    // Dispatcher para operacoes de I/O
    launch(Dispatchers.IO) {
        println("IO: ${Thread.currentThread().name}")
    }

    // Dispatcher da thread principal (Android/Swing/JavaFX)
    // launch(Dispatchers.Main) { ... }

    // Dispatcher sem confinamento - nao garante thread especifica
    launch(Dispatchers.Unconfined) {
        println("Unconfined: ${Thread.currentThread().name}")
    }
}

Dispatchers.Default

O Dispatchers.Default usa um pool de threads compartilhado com tamanho igual ao numero de nucleos da CPU (minimo 2). E otimizado para trabalho intensivo de CPU:

suspend fun calcularPrimos(ate: Int): List<Int> = withContext(Dispatchers.Default) {
    (2..ate).filter { numero ->
        (2..Math.sqrt(numero.toDouble()).toInt()).none { numero % it == 0 }
    }
}

fun main() = runBlocking {
    val primos = calcularPrimos(100_000)
    println("Encontrados ${primos.size} primos")
}

Use Dispatchers.Default para: algoritmos, processamento de dados, serializacao/desserializacao, calculos matematicos.

Dispatchers.IO

O Dispatchers.IO usa um pool de threads maior (padrao de 64 threads) projetado para operacoes de entrada e saida que bloqueiam a thread:

suspend fun lerArquivo(caminho: String): String = withContext(Dispatchers.IO) {
    java.io.File(caminho).readText()
}

suspend fun buscarDaApi(url: String): String = withContext(Dispatchers.IO) {
    java.net.URL(url).readText()
}

suspend fun consultarBanco(): List<String> = withContext(Dispatchers.IO) {
    // Simula consulta ao banco
    Thread.sleep(1000)
    listOf("resultado1", "resultado2")
}

Use Dispatchers.IO para: leitura/escrita de arquivos, chamadas de rede, acesso a banco de dados, chamadas a APIs externas.

Dispatchers.Main

O Dispatchers.Main executa na thread principal da aplicacao. Em Android, e a thread de UI. Em aplicacoes desktop com Swing ou JavaFX, e a thread de eventos:

// Android: atualizar UI apos buscar dados
suspend fun carregarEExibir() {
    val dados = withContext(Dispatchers.IO) {
        repositorio.buscarDados()
    }
    // Volta para Main automaticamente
    textView.text = dados.toString()
}

Em Android com viewModelScope ou lifecycleScope, o dispatcher padrao ja e Main, entao voce so precisa mudar explicitamente quando for fazer I/O ou CPU.

Dispatchers.Unconfined

O Dispatchers.Unconfined inicia a coroutine na thread do chamador, mas apos a primeira suspensao, retoma na thread que completou a operacao suspensa:

fun main() = runBlocking {
    launch(Dispatchers.Unconfined) {
        println("Antes: ${Thread.currentThread().name}")
        delay(100)
        println("Depois: ${Thread.currentThread().name}") // Pode ser outra thread!
    }
}

Use com extrema cautela. O Unconfined e util em testes e casos muito especificos, mas pode causar comportamento imprevisivel em codigo de producao.

withContext: trocando de dispatcher

A funcao withContext troca o dispatcher dentro de uma coroutine sem criar uma nova:

suspend fun processarDados(): String {
    // Busca dados em IO
    val dadosBrutos = withContext(Dispatchers.IO) {
        repositorio.buscarDados()
    }

    // Processa em Default
    val dadosProcessados = withContext(Dispatchers.Default) {
        dadosBrutos.map { transformar(it) }
    }

    return dadosProcessados.joinToString()
}

withContext e mais eficiente que launch + join porque nao cria uma nova coroutine; apenas muda o contexto de execucao.

Criando dispatchers customizados

Para cenarios especificos, voce pode criar seus proprios dispatchers:

// Dispatcher com thread unica (util para acesso sequencial)
val dispatcherDeBanco = newSingleThreadContext("BancoDeDados")

// Dispatcher com pool fixo
val dispatcherDeProcessamento = newFixedThreadPoolContext(4, "Processamento")

// Converter um Executor em dispatcher
val meuExecutor = java.util.concurrent.Executors.newCachedThreadPool()
val dispatcherCustom = meuExecutor.asCoroutineDispatcher()

Lembre-se de fechar dispatchers customizados quando nao forem mais necessarios para evitar vazamento de threads:

dispatcherDeBanco.close()
dispatcherDeProcessamento.close()

limitedParallelism

A partir do Kotlin 1.6, voce pode criar uma visao limitada de um dispatcher existente:

// Limita IO a no maximo 4 threads para uma operacao especifica
val dispatcherLimitado = Dispatchers.IO.limitedParallelism(4)

suspend fun processarArquivos(arquivos: List<String>) = coroutineScope {
    arquivos.map { arquivo ->
        async(dispatcherLimitado) {
            lerEProcessar(arquivo)
        }
    }.awaitAll()
}

Isso e util para evitar que uma operacao consuma todas as threads do pool de I/O, deixando outras operacoes sem recursos.

Quando usar cada dispatcher

DispatcherUsoExemplos
DefaultCPU intensivoCalculos, parsing, serializacao
IOBloqueio de I/ORede, arquivos, banco de dados
MainUIAtualizar tela, mostrar dialogo
UnconfinedTestesTestes unitarios simples

Erros comuns

  1. Fazer I/O no Dispatchers.Default: operacoes que bloqueiam a thread desperdicam os poucos threads do pool Default. Use IO para operacoes bloqueantes.

  2. Atualizar UI fora do Dispatchers.Main: em Android, acessar Views fora da main thread causa crash. Sempre volte para Main antes de atualizar a UI.

  3. Usar Dispatchers.IO para CPU: IO tem muitas threads, o que e desperdicio para trabalho de CPU. O excesso de context switching reduz a performance.

  4. Criar dispatchers customizados sem necessidade: os dispatchers padroes atendem a grande maioria dos casos. Crie customizados apenas quando tiver necessidades especificas de controle.

  5. Nao fechar dispatchers customizados: dispatchers criados com newSingleThreadContext ou newFixedThreadPoolContext precisam ser fechados para liberar threads.

  6. Confundir withContext com launch: withContext suspende e retorna um resultado; launch cria uma coroutine que roda em paralelo. Use withContext para trocar de dispatcher e continuar sequencialmente.

Termos relacionados

  • Coroutine: unidade de execucao leve que roda em um dispatcher especifico.
  • CoroutineContext: contexto que inclui o dispatcher, o Job e outros elementos.
  • withContext: funcao que muda o dispatcher dentro de uma coroutine.
  • launch/async: funcoes que criam novas coroutines, opcionalmente com um dispatcher especifico.
  • Job: elemento do contexto que representa o ciclo de vida da coroutine.
  • Flow: streams reativos que podem mudar de dispatcher com flowOn.

Dispatchers sao o elo entre coroutines e threads. Entender quando e como usa-los e essencial para escrever codigo concorrente que seja eficiente e livre de bugs de threading.