O que é Dispatcher em Kotlin?

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

Escolher o dispatcher correto e fundamental para a performance é o comportamento da aplicação. Uma operação de rede executada no dispatcher errado pode travar a interface do usuário; um calculo pesado no dispatcher errado pode desperdicar recursos.

Os dispatchers principais

O Kotlin fornece quatro dispatchers padrões através 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 número 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, serialização/desserializacao, calculos matematicos.

Dispatchers.IO

O Dispatchers.IO usa um pool de threads maior (padrão de 64 threads) projetado para operações 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 aplicação. Em Android, e a thread de UI. Em aplicações 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 padrão já e Main, entao você só 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 operação 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 útil em testes e casos muito específicos, mas pode causar comportamento imprevisivel em código de producao.

withContext: trocando de dispatcher

A função 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 não cria uma nova coroutine; apenas muda o contexto de execução.

Criando dispatchers customizados

Para cenários específicos, você pode criar seus próprios 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 não forem mais necessários para evitar vazamento de threads:

dispatcherDeBanco.close()
dispatcherDeProcessamento.close()

limitedParallelism

A partir do Kotlin 1.6, você 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 útil para evitar que uma operação consuma todas as threads do pool de I/O, deixando outras operações sem recursos.

Quando usar cada dispatcher

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

Casos de Uso no Mundo Real

  1. aplicações Android com chamadas de API: em apps Android, o padrão mais comum e usar Dispatchers.IO para chamadas de rede e banco de dados, e Dispatchers.Main para atualizar a interface do usuário com os resultados. ViewModels tipicamente lancam coroutines no Main e trocam para IO com withContext quando necessário.

  2. Servidores backend com Ktor ou Spring: em aplicações server-side, Dispatchers.IO e usado para operações de banco de dados e chamadas a servicos externos, enquanto Dispatchers.Default processa lógica de negócio intensiva como geracao de relatorios, compressao de dados ou transformacoes em lote.

  3. Processamento de imagens e arquivos: aplicações que manipulam imagens ou processam grandes volumes de arquivos usam Dispatchers.Default para operações de CPU (redimensionamento, compressao) e Dispatchers.IO para leitura e escrita no disco, frequentemente combinados com limitedParallelism para controlar o uso de recursos.

  4. Pipelines de dados em tempo real: sistemas que consomem dados de sensores, WebSockets ou filas de mensagens utilizam dispatchers customizados com newSingleThreadContext para garantir processamento sequencial e thread-safe, combinados com Dispatchers.Default para etapas de transformacao paralela.

Boas Praticas

  • Use withContext em vez de launch quando precisar apenas trocar de dispatcher e aguardar o resultado. Isso evita a criação desnecessaria de uma nova coroutine e mantém o fluxo sequencial.
  • Encapsule a escolha do dispatcher dentro de funções suspend de repositórios e data sources, em vez de exigir que o chamador saiba qual dispatcher usar. Isso segue o princípio de responsabilidade única.
  • Utilize limitedParallelism para controlar o grau de concorrencia em operações que acessam recursos limitados, como conexoes de banco de dados ou APIs com rate limiting.
  • Evite Dispatchers.Unconfined em código de producao. Ele e útil para testes unitarios simples, mas em producao pode causar comportamento imprevisivel ao retomar a coroutine em threads inesperadas.
  • Injete dispatchers como dependência em classes que os utilizam, permitindo que testes substituam por TestDispatcher para execução deterministica e controlada.

Perguntas Frequentes

P: Qual a diferenca entre Dispatchers.Default e Dispatchers.IO? R: O Dispatchers.Default usa um pool de threads limitado ao número de nucleos da CPU e e otimizado para trabalho computacional intensivo. O Dispatchers.IO usa um pool maior (até 64 threads por padrão) e e projetado para operações que bloqueiam a thread, como leitura de arquivos ou chamadas de rede. Usar o dispatcher errado pode causar problemas de performance: CPU intensivo no IO desperdiça context switching, e I/O bloqueante no Default pode esgotar os poucos threads disponiveis.

P: Preciso sempre especificar um dispatcher ao lancar uma coroutine? R: Nao. Se você não especificar, a coroutine herda o dispatcher do escopo pai. Em Android com viewModelScope, o dispatcher padrão e Dispatchers.Main. Em runBlocking, e o dispatcher da thread que chamou. Especifique um dispatcher apenas quando precisar de comportamento diferente do herdado.

P: Como escolher entre criar um dispatcher customizado e usar limitedParallelism? R: Prefira limitedParallelism na maioria dos casos, pois ele reutiliza threads do pool existente sem criar threads extras. Crie dispatchers customizados apenas quando precisar de isolamento total de threads, como em operações que exigem thread-local storage ou acesso a bibliotecas nativas que não sao thread-safe.

P: E seguro compartilhar o mesmo dispatcher entre diferentes partes da aplicação? R: Sim, os dispatchers padrão (Default, IO, Main) sao projetados para uso compartilhado global. Para dispatchers customizados, o compartilhamento e seguro desde que você gerencie o ciclo de vida corretamente e feche o dispatcher quando a aplicação não precisar mais dele.

Erros comuns

  1. Fazer I/O no Dispatchers.Default: operações que bloqueiam a thread desperdicam os poucos threads do pool Default. Use IO para operações 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 é desperdicio para trabalho de CPU. O excesso de context switching reduz a performance.

  4. Criar dispatchers customizados sem necessidade: os dispatchers padrões atendem a grande maioria dos casos. Crie customizados apenas quando tiver necessidades específicas 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 execução leve que roda em um dispatcher específico.
  • CoroutineContext: contexto que inclui o dispatcher, o Job e outros elementos.
  • withContext: função que muda o dispatcher dentro de uma coroutine.
  • launch/async: funções que criam novas coroutines, opcionalmente com um dispatcher específico.
  • Job: elemento do contexto que representa o ciclo de vida da coroutine.
  • Flow: streams reativos que podem mudar de dispatcher com flowOn.

Dispatchers são o elo entre coroutines e threads. Entender quando e como usa-los e essencial para escrever código concorrente que seja eficiente e livre de bugs de threading.