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
| Dispatcher | Uso | Exemplos |
|---|---|---|
| Default | CPU intensivo | Calculos, parsing, serializacao |
| IO | Bloqueio de I/O | Rede, arquivos, banco de dados |
| Main | UI | Atualizar tela, mostrar dialogo |
| Unconfined | Testes | Testes unitarios simples |
Erros comuns
Fazer I/O no Dispatchers.Default: operacoes que bloqueiam a thread desperdicam os poucos threads do pool Default. Use IO para operacoes bloqueantes.
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.
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.
Criar dispatchers customizados sem necessidade: os dispatchers padroes atendem a grande maioria dos casos. Crie customizados apenas quando tiver necessidades especificas de controle.
Nao fechar dispatchers customizados: dispatchers criados com
newSingleThreadContextounewFixedThreadPoolContextprecisam ser fechados para liberar threads.Confundir withContext com launch:
withContextsuspende e retorna um resultado;launchcria uma coroutine que roda em paralelo. UsewithContextpara 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.