Skip to main content

Isolate e Compute

  • Isolate e Compute

Isolate

Todo o código Dart é executado isoladamente. A implementação de vários isolados para permitir a simultaneidade em seus programas Dart depende de você. Como você pode implementar vários isolados depende de nós da equipe Dart e, no Dart 2.19, fizemos uma grande atualização no processo que estamos entusiasmados em compartilhar. Encontrar Isolate.run()!

run()pega toda a complexidade de configuração e gerenciamento de isolados e os abstrai completamente em uma única chamada de função. Já faz algum tempo que é possível usar isolados com um punhado de primitivos. Mas, mesmo com todas as recentes melhorias de desempenho para isolados, o processo era, na melhor das hipóteses, tedioso e, na pior das hipóteses, sujeito a erros.

Para avaliar o quanto run()é uma melhoria, este artigo analisa o método anterior de construção da funcionalidade peça por peça a partir de primitivas de baixo nível. Em seguida, contrastaremos isso com using Isolate.run()e mostraremos como funciona por dentro. Mesmo que você nunca tenha usado isolados antes, temos certeza de que run()você ficará animado para experimentá-los!

Isolados

Isolados são um conceito bastante simples. Um isolado é essencialmente um único thread de execução no Dart. Eles permitem que você execute partes do seu código em paralelo. Você pode iniciar novas execuções paralelas (quantas desejar) e dizer a elas o que fazer, diretamente main(do thread principal ou do isolamento principal ). Os isolados não compartilham memória; em vez disso, eles passam mensagens para se comunicar. Portanto, você não precisa se preocupar com problemas típicos de multithreading, como condições de corrida ou mutexes e bloqueios.

https://miro.medium.com/v2/resize:fit:613/0*eCogwJXZtpbg5Hbq.png

Parece bom! Mas como você os usa? Antes Isolate.run(), era aí que as coisas ficavam complicadas.

A API isolada é composta de primitivas de baixo nível que fornecem uma ampla gama de funcionalidades. Granularidade como essa é ótima quando você precisa personalizar a funcionalidade de seus isolados. Porém , a granularidade não é tão grande quando é necessário usar isolados . Especialmente porque quase todos os casos de uso de isolados exigem as mesmas configurações básicas para configuração e gerenciamento. Isso basicamente significava expor todos os detalhes da implementação para vocês, usuários do Dart, cuidarem de si mesmos.

Vamos dar uma olhada em uma configuração isolada típica para entender melhor o tedioso processo que Isolate.run()a resolve.

Usando isolados (antes)

Você pode considerá-lo [Isolate.spawn()](https://api.dart.dev/stable/dart-isolate/Isolate/spawn.html)o antigo ponto de partida para isolados. A função de computação do Flutter foi construída a partir do spawn(). É necessário um método para invocar como entrypointargumento, além de quaisquer argumentos para esse método e outras configurações para o próprio isolado. No passado, este entrypointsó poderia ser um método estático ou de nível superior.

Isolate.spawn(_readAndParseJson, filename);

Isolado criado, tudo pronto! Estou brincando. Nem mesmo perto.

Ligar spawnnão retornará nada para realmente trabalhar. Ele retorna apenas um objeto isolado, que simplesmente confirma que o isolamento foi iniciado.

O isolado principal e o isolado gerado (o isolado criado por spawn()) não podem se comunicar diretamente, além dos argumentos iniciais passados na criação. Realisticamente, mesmo que você não precise de nenhum resultado de cálculo do isolado gerado, você ainda desejaria algum tipo de verificação de que o cálculo foi bem-sucedido, portanto, sempre desejará uma mensagem de volta.

Para ativar a comunicação, você precisa configurar portas. Você cria um ReceivePortobjeto antes de chamar o spawn. O sendPortmembro do ReceivePortobjeto vai para o isolado gerado como outro argumento para spawn.

void main() async {
final resultPort = ReceivePort();
final jsonData = await Isolate.spawn(_readAndParseJson, [resultPort.sendPort, filename]);
}

Isso significa que a função para a qual você passa spawndeve ser configurada especificamente para fazer algo com isso sendPort. Em outras palavras, você não pode simplesmente reutilizar funções existentes com isolados. Então, em vez de passar spawnuma função regular que apenas lê e analisa um arquivo JSON, você criaria algo como:

Future<void> _readAndParseJson(List<dynamic> args) async {
SendPort responsePort = args[0];
String fileName = args[1];

final fileData = await File(filename).readAsString();
final result = jsonDecode(fileData);
// ...
}

Sua função especial de decodificação JSON, amigável ao isolamento, pode simplesmente “ return result” e pronto, certo? Não exatamente. O resultado precisa ser enviado por meio de responsePort. É assim que os isolados se comunicam com as portas. Você pode usar outra primitiva [Isolate.exit()](https://api.dart.dev/stable/dart-isolate/Isolate/exit.html)para retornar o resultado de forma eficiente e fechar o isolado gerado ao mesmo tempo:

Isolate.exit(responsePort, result);

exit()função transfere a memória que contém a mensagem no isolado gerado para o isolado principal (em vez de copiá-la) e fecha o isolado com segurança.

Vamos juntar tudo isso. Como resultneste exemplo o JSON é analisado, você provavelmente desejaria desestruturá-lo um pouco para realmente usá-lo. Por uma questão de código limpo, em vez de plantar aquelas três linhas que criam o receivePorte o isolado, e aguardar sua resposta, direto em main(), deveríamos colocá-las em sua própria função.

void main() async {
final jsonData = await _spawnAndReceive(filename);
print('Received JSON with ${jsonData.length} keys');
}

// Spawns an isolate and waits for the first message
Future<Map<String, dynamic>> _spawnAndReceive(String filename) async {
final resultPort = ReceivePort();
await Isolate.spawn(_readAndParseJson, [resultPort.sendPort, filename]);

return (await resultPort.first) as Map<String, dynamic>;
}

// The entrypoint that runs on the spawned isolate.
void _readAndParseJson(List<dynamic> args) async {
SendPort responsePort = args[0];
String fileName = args[1];

final fileData = await File(filename).readAsString();
final result = jsonDecode(fileData) as Map<String, dynamic>;

Isolate.exit(responsePort, result);
}

O que está sendo realizado é relativamente simples. É a exposição dos detalhes de implementação que faz com que pareça complicado, como portas para passagem de mensagens e a necessidade de uma função dedicada para lidar com portas que, de outra forma, seriam independentes do isolamento.

Manipulação de erros

O exemplo até agora ainda não é realmente uma implementação “completa” e pronta para produção. Você estaria prestando um péssimo serviço a si mesmo se deixasse de lado qualquer tipo de tratamento de erros, mas isso geralmente é esquecido como uma etapa adicional em um processo já substancial. Sem qualquer tratamento de erros, se um erro assíncrono não detectado travasse seu isolamento, por exemplo, você não saberia o que aconteceu para causar o erro, nem mesmo seria alertado de que algo havia acontecido .

Cobrir todas as possibilidades de tratamento de erros para isolados seria extenso, mas em geral envolveria mais algumas adições ao código.

Você poderia, no mínimo, adicionar os parâmetros errorsAreFatalonExitonErrorà chamada spawn:

await Isolate.spawn(_readAndParseJson, [resultPort.sendPort, filename],
onError: resultPort.sendPort,
onExit: resultPort.sendPort,
errorsAreFatal: true
);

Isso garante que resultPortuma mensagem seja recebida mesmo que o isolado gerado termine sem enviar um resultado ou se houver algum erro não detectado. Tornar os erros fatais significa que os erros não detectados saem do isolado como precaução de segurança, para garantir que ele termine.

onExitargumento faz com que o isolado seja enviado nullpara a porta ao sair. O onErrorargumento faz com que um erro não detectado envie uma lista de duas strings para a porta (a toStringdo erro e do rastreamento de pilha).

A reutilização da porta de resultados permite evitar a criação de mais portas, portanto, você só precisa procurar mensagens em um só lugar. Mas também significa que você precisa distinguir as mensagens onExitonErrordo valor do resultado. Aqui, assumimos que o JSON deve ser um Map, portanto não pode ser uma lista ou null. Caso contrário, você também teria que embrulhar o resultado em algo reconhecível. Você teria que criar um protocolo de mensagens (trivial) sobre as mensagens da porta.

Além desse mínimo, você poderá verificar responseerros específicos. Um desses casos seria verificar if resultPortis null, ou seja, o isolamento foi encerrado sem enviar resultados:

final response = await resultPort.first;
if (response == null) {
// Isolate exited. Throw some error, for example:
throw RemoteError("Isolate terminated without result");
}

Outra seria verificar se o resultado é uma lista, o que significa que ocorreu um erro não detectado:

if (response is List<dynamic>) {
// Uncaught error, list of two strings.
var remoteError = RemoteError(response[0], response[1]);
await Future.error(remoteError); // Rethrow.
}

Então, finalmente, lide com um resultado real:

// Otherwise it's a result value, which is a map.
return message as Map<String, dynamic>;

De qualquer forma, você gostaria de colocar spawnum trybloco para verificar se o envio entrypointpara o novo isolado falhou. Se isso acontecer, a porta de resultado não receberá nenhuma mensagem e precisará ser fechada:

try {
await Isolate.spawn( ... );
} on Object {
resultPort.close();
rethrow;
}

Fornecer um mínimo de tratamento de erros garante que a porta de resultados esteja sempre fechada e _spawnAndReceievesempre seja concluída, não importa como o isolado gerado saia. Você também pode tornar as coisas mais agradáveis , por exemplo, capturando e enviando erros e rastreamentos de pilha de volta como objetos reais, não apenas strings como o onErrormanipulador.

O tratamento de erros obviamente introduz muita variação e, com isso, a sobrecarga mental de decidir como abordá-lo e o que levar em consideração. É compreensível que geralmente seja deixado de fora da configuração primitiva de isolamento.

Usando isolados (depois)

Isolate.run()configura todas as partes da implementação isolada com as primitivas que você mesmo precisava usar anteriormente, em uma única chamada de função:

void main() async {
final jsonData = await Isolate.run(_readAndParseJson);
print('Received JSON with ${jsonData.length} keys');
}

Sem portas, sem geração, saída ou tratamento de erros separados e sem estrutura de retorno especial. Talvez a melhor parte para a qual entrypointvocê passa runpossa ser qualquer função existente:

Future<Map<String, dynamic>> _readAndParseJson() async {
final fileData = await File(filename).readAsString();
return jsonDecode(fileData);
}

Este exemplo mostra uma função assíncrona , mas runpoderia facilmente executar uma função síncrona . A runfunção em si sempre retorna de forma assíncrona, o que é tudo que importa.

Também pode entrypointser uma expressão de função , escrita diretamente em linha onde quer que você chame run. Isolados e quaisquer APIs de nível superior escritas no topo não estão mais limitadas à execução apenas de funções estáticas ou de nível superior.

final jsonData = await Isolate.run(() async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
return jsonData;
});

O parâmetro de mensagem adicional não é mais necessário e você pode evitar empacotar e descompactar argumentos em estruturas de dados como listas.

Você não precisa pensar muito sobre o tratamento de erros. A runfunção combina captura, tratamento e comunicação de isolamento cruzado de erros locais e remotos e expõe o resultado como um único erro normal (assíncrono) que você pode capturar em um trycatch. Você pode esquecer os isolados e tratá-los como uma função normal.

try {
await Isolate.run(_readAndParseJson);
} on FormatException catch (e, s) {
print(e.message);
}

Isolate.run()permite um código muito mais limpo e ergonômico. A função do Flutter computeaté passou a usar runem vez de spawn!

Dentro de Isolate.run()

Dê uma olhada na [implementação emrun](https://api.dart.dev/dev/3.0.0-125.0.dev/dart-isolate/Isolate/run.html#implementation:~:text=isolate%20for%20debugging.-,Implementation,-%40Since(%222.19) si. Ele se aprofunda em todas as APIs relacionadas ao isolamento de baixo nível (que seria seu trabalho implementar antes) para construir uma configuração de isolamento abrangente e “perfeita”. Ele usa o método para executar computatione configura todas as portas e seus retornos para levar em conta a passagem eficiente de mensagens entre os isolados.

static Future<R> run<R>(FutureOr<R> computation(), {String? debugName}) {
var result = Completer<R>();
var resultPort = RawReceivePort();
// ...
result.complete; // or result.completeError for error handling blocks
// ...

Há um tratamento completo de erros para cada caso potencial. A runfunção verifica se um isolado morre antes de terminar a computação. Se o cálculo for lançado, o isolado termina e lança o mesmo erro para o isolado principal.

// ...
resultPort.handler = (response) {
resultPort.close();
if (response == null) {
// onExit handler message, isolate terminated without sending result.
result.completeError(
RemoteError("Computation ended without result", ""),
StackTrace.empty);
return;
}
// ...

Se ocorrer um erro assíncrono não detectado, o isolado termina e reporta o erro de forma assíncrona ao isolado principal. Se o isolado principal terminar primeiro, o isolado gerado terminará e tratará a situação como um erro assíncrono não detectado.

// ...
void _run() async {
R result;
try {
var potentiallyAsyncResult = computation();
if (potentiallyAsyncResult is Future<R>) {
result = await potentiallyAsyncResult;
} else {
result = potentiallyAsyncResult;
}
} catch (e, s) {
// If sending fails, the error becomes an uncaught error.
Isolate.exit(resultPort, _list2(e, s));
}
Isolate.exit(resultPort, _list1(result));
}

Por último, runsempre usa exitpara desligar com segurança. Isso significa que os dados são transferidos com eficiência de um isolado para outro, sem realmente copiá-los.

Resumo

runfunção é ideal para iniciar um cálculo e aguardar os resultados. As primitivas ainda estão lá se você quiser criar sua própria configuração de isolamento para algo que runnão cobre, como um isolado de longa duração que pode enviar e receber mensagens várias vezes. Porém, na maioria dos casos, a substituição spawne todas as suas configurações de suporte por uma única runinstrução devem ser usadas em vez de qualquer outra configuração.

Se você nunca experimentou o gerenciamento isolado antes run, seria difícil acreditar que toda essa funcionalidade teve que ser implementada anteriormente pelo usuário! Isolate.run()- disponível no Dart 2.19 e Flutter 3.7 - torna o código muito mais ergonômico e torna os isolados muito mais fáceis de usar. O que você fará com todo o tempo runque você economiza?

Compute

  • No Flutter, o compute é uma função que permite a execução de funções assíncronas em isolamentos, também conhecidos como isolates. Isolates são threads leves que executam código em paralelo, permitindo que você realize operações intensivas em termos de CPU sem bloquear a thread principal da interface do usuário. O uso do compute é especialmente útil quando você precisa realizar cálculos demorados, processamento intenso ou operações assíncronas, mas deseja evitar o congelamento da interface do usuário durante essas operações. A assinatura da função compute é a seguinte:
Future<R> compute<R, T>(R Function(T) callback, T message, { String debugLabel });
  • callback: A função que será executada no isolamento.
  • message: Os argumentos que serão passados para a função callback.
  • debugLabel: Opcional. Uma string de rótulo de depuração que pode ser usada para identificar a computação em logs de depuração. Exemplo de uso:
import 'package:flutter/foundation.dart';

Future<int> add(int value) async {
// Simulando uma operação demorada
await Future.delayed(Duration(seconds: 2));
return value + 1;
}

void main() async {
final result = await compute(add, 5);
print(result); // Imprime: 6
}
  • Neste exemplo, a função add é executada em um isolate usando compute. Enquanto a função add está sendo executada, a thread principal do Flutter pode continuar a responder a eventos de interface do usuário, evitando que a aplicação pareça travada durante operações demoradas. Lembre-se de que, ao usar compute, os dados passados para a função e os resultados devem ser serializáveis, pois ocorre a comunicação entre isolates. Isso significa que você não pode passar funções ou objetos complexos diretamente. Além disso, qualquer estado compartilhado entre isolates deve ser gerenciado cuidadosamente para evitar condições de corrida.

Isolate, Compute, Thread e Async/Await

🧠 O que é um Isolate?

  • Em Dart, isolates são unidades de execução que não compartilham memória.
  • Ideal para tarefas pesadas que podem travar a UI se feitas na thread principal.

🔧 Como funciona?

import 'dart:isolate';

void tarefaPesada(SendPort sendPort) {
// Simula uma tarefa pesada
int resultado = 0;
for (int i = 0; i < 100000000; i++) {
resultado += i;
}

sendPort.send(resultado);
}

void main() async {
ReceivePort receivePort = ReceivePort();

await Isolate.spawn(tarefaPesada, receivePort.sendPort);

receivePort.listen((message) {
print('Resultado: $message');
receivePort.close(); // Fechar depois de receber
});
}

📌 Pontos-chave:

  • Isolate.spawn cria uma nova "thread" de execução.
  • Comunicação entre isolates é feita com SendPort e ReceivePort.

2. Quando usar compute()

📦 O que é compute()?

  • Uma função utilitária do Flutter para usar isolates de forma simplificada.
  • Bom para tarefas puras (sem acessar contexto/UI).

🛠 Exemplo:

import 'package:flutter/foundation.dart';

int tarefaPesada(int n) {
int resultado = 0;
for (int i = 0; i < n; i++) {
resultado += i;
}
return resultado;
}

void main() async {
final resultado = await compute(tarefaPesada, 100000000);
print('Resultado: $resultado');
}

✅ Quando usar:

  • Funções puras e isoláveis.
  • Sem dependência de contexto, como acesso a bancos de dados ou estado da UI.

3. Diferença entre thread, isolate e async/await

ConceitoExplicação
ThreadExecução paralela com memória compartilhada (não existe diretamente em Dart).
IsolateExecução paralela sem memória compartilhada (modelo do Dart).
async/awaitConcor­rência não paralela – útil para I/O ou delays, mas ainda na mesma thread.

⚠️ async/await ≠ paralelo

Usar await não paraleliza — apenas espera sem travar a UI.


4. Worker threads para heavy tasks

👷‍♂️ Quando usar isolates/compute():

  • Parsing de JSON muito grande.
  • Compressão de imagens/arquivos.
  • Criptografia, hashing.
  • Algoritmos matemáticos pesados (ex: simulação, análise de dados).

⚠️ Dica:

Evite fazer essas tarefas pesadas na UI thread (o main() do Flutter), pois causam travamentos.


🔚 Resumo Rápido

Caso de usoSolução Ideal
Tarefa leve com awaitasync/await
Tarefa pura e pesadacompute()
Controle total e flexívelIsolate.spawn
UI depende da tarefaUse isolate + comunicação via SendPort