Isolated_agents: Isolados fáceis para Flutter
Este artigo apresenta um novo pacote para Flutter e Dart que torna os isolados mais fáceis de trabalhar. Ajuda qualquer pessoa que queira descarregar o trabalho de um isolado para outro. Se você quiser pular adiante, verifique o novo pacote Isolated_agents.
Conteudo
Fundo
À medida que os projetos Flutter amadurecem e se tornam mais complexos, não é incomum começar a ver gargalos no thread de IU do mecanismo Flutter. O recurso async/await
do Dart é ótimo para trabalhar com APIs assíncronas, mas não ajuda a escrever um novo código que é executado fora do thread da interface do usuário. No início da história do Dart, Isolados foram adicionados para resolver esse problema. A Wikipedia classifica o Dart como uma linguagem Actor Model e, embora o Dart tenha regiões isoladas de memória associadas a threads de execução, ele não possui um protocolo ou API predefinido para comunicação entre eles.
Para usar um Isolate de qualquer maneira mais complicada do que apenas “disparar uma tarefa e receber uma notificação quando concluída”, é preciso:
- Desenvolva um protocolo para como o Isolate comunicará entrada e saída, por exemplo, como os dados são codificados na API SendPort?
- Execute um handshake SendPort para estabelecer um canal de comunicação.
- Gerencie uma fila de manipuladores a serem executados em série quando os resultados forem calculados.
- Elabore um protocolo de tratamento de erros.
- Elabore um protocolo de desligamento.
A equipe do Flutter reconheceu que o uso de Isolates é complicado e adicionou a função de computação. Isso torna o caso de uso “dispare uma tarefa e receba uma notificação quando ela for concluída” mais fácil de escrever, mas abstrai completamente o Isolate, que não dá ao desenvolvedor controle total sobre o desempenho de seu código. Você não pode controlar a sobrecarga de gerar um estado isolado ou armazenado no isolado. Ambos os fatos contribuem para um desempenho abaixo do ideal, quando o objetivo principal da função era melhorar o desempenho.
Se ao menos houvesse uma API de nível superior em cima de um Isolate que mantivesse o controle do Isolate e a facilidade de uso da computação… Isso poderia simplificar as tarefas de multiprocessamento e tornar as tarefas de multiprocessamento complexas realizáveis.
Agentes Clojure
Ao abordar uma solução para o problema do trabalho extra que o Dart Isolates exige para ser útil, voltei a minha experiência com a linguagem de programação Clojure. Clojure é uma linguagem de modelo de ator funcional cujos dados são imutáveis por padrão. Ele foi projetado com o multiprocessamento pesado em mente e não tem o mesmo problema com multiprocessamento detalhado e difícil que o Dart tem. O designer adotou uma abordagem ligeiramente diferente para os atores do que as linguagens como Erlang, em vez de se inspirar em linguagens funcionais. Em Clojure, o conceito de agente é um dado associado a um thread. Nesse sentido, é semelhante ao Dart Isolates, mas ao contrário do Dart Isolates, os agentes têm um protocolo, canal e mecanismo de resposta estabelecidos. O protocolo envia encerramentos para o thread e os executa lá — os resultados substituem o estado mantido.
Um uso simples de um Agente:
; Create the thread with state = 0 (let [foo (agent 0)] ; Execute x + 1 on the background thread and assign it ; to the value associated with the thread. (send foo (fn [x] (+ x 1))) (send foo (fn [x] (+ x 1))) ; Request and wait for the value from `foo` and print it. (print (deref foo)))
Pacote isolado_agentes
O novo pacote, Isolated_agents, implementa um padrão semelhante ao do agente Clojure mencionado anteriormente. Ele cria um protocolo padronizado para comunicação entre isolados e elimina os apertos de mão e o gerenciamento de estado que você deve executar para usá-los com eficácia.
Aqui está o mesmo exemplo acima, escrito em Dart com o pacote Isolated_agents:
void main() async { // Create the thread with state = 0 Agent<int> foo = await Agent.create(() => 0); // Execute 1 + 2 on the background thread and assign it // to the value associated with the thread. foo.update((x) => x + 1); foo.update((x) => x + 1); // Request and wait for the value from `foo` and print it. print(await foo.exit()); }
Padrões de agente
O exemplo anterior com o uso de Isolated_agents
foi muito simples. Aqui estão alguns padrões mais interessantes que podem ser realizados usando Agentes como blocos de construção.
Manipulador de plano de fundo de longa duração
O método de computação gera e mata um novo Dart Isolate: isso incorre em sobrecarga ao girar o Isolate. Você pode eliminar essa sobrecarga usando um Agente de longa duração:
// An agent that lives for the duration of the main isolate. final Future<Agent<String>> _agent = Agent.create(‘’); Future<String> _receiveEncryptedMessage(String encrypted) async { Agent<String> agent = await _agent; agent.update((_) => _decrypt(encrypted)); // Notice the use of `read` (not `kill`) to keep the agent alive. return agent.read(); }
Gerenciamento de estado de isolamento cruzado
Gerenciar o estado mutável em vários isolados é complicado, mas atribuir um Agente para gerenciar o estado facilita:
// Executed on the root Isolate. void _addUser(Agent<DataModel> model, String name) { model.update((database) { database.addUser(name); }); } // Autosave writing to disk by periodically executing on // the background isolate. void _autosave(Agent<DataModel> model) { DataModel db = await agent.read(); db.saveToDisk(); }
Pipeline
Imagine que você deseja configurar um pipeline em que um trabalho de trabalho está sendo preparado enquanto outro está sendo atendido. Neste exemplo, um Isolate gera uma árvore que é simultaneamente renderizada como uma imagem por outro Isolate:
class Pipeline { Pipeline._(this.builder, this.renderer); final Agent<Tree?> builder; final Agent<Object?> renderer; static Future<Pipeline> create() async {...} Future<Image> process(int index) { ReceivePort receiver = ReceivePort(); SendPort sender = receiver.sender; builder.update((_) => _buildTree(index)); builder.update((tree) { renderer.update((_) { Image image = _buildImage(tree); sender.send(image); }); return null; }); Object? object = await receiver.first; return object! as Image; } } Future<List<Image>> _startJobs(Pipeline pipeline) { List<Future<Image>> images = []; for (int i = 0; i < 10; ++i) { images.add(pipeline.process(i)); } return Future.wait(images); }
Cache de operação em segundo plano
Este caso de uso tem um cálculo pesado que precisa ser executado em um fundo isolado, mas o resultado deve ser armazenado em cache. Desta forma, o mesmo cálculo nunca é feito duas vezes, e novos cálculos podem ser baseados em cálculos anteriores:
final Future<Agent<WorldMap>> _agent = Agent.create(() => WorldMap()); Future<LocalMap> getLocalMap(int latitude, int longitude) async { Agent<WorldMap> agent = await _agent; agent.update((worldMap) => { if (!worldMap.hasLocalMap(latitude, longitude)) { // Perform heavy loading of the map on background isolate and memoize // the result. worldMap.loadLocalMap(latitude, longitude); } return worldMap; }); return agent.read(query: (worldMap) => worldMap.getLocalMap(latitude, longitude)); }
Exemplo em Flutter
Você pode verificar um exemplo que usa isolado_agentes no projeto romeo_juliet no GitHub.
O aplicativo executa as seguintes etapas:
- Carregue o recurso de texto com todas as mensagens descriptografadas, divida-as, criptografe-as e grave-as no diretório de documentos.
- Consultar o diretório de documentos da plataforma e armazená-lo no agente.
- Inicie um cronômetro que (a cada segundo) acione um trabalho para ler a mensagem criptografada do diretório de documentos e descriptografá-la.
- Quando o trabalho terminar, se tivermos uma nova mensagem descriptografada, adicione-a ao cache na raiz isolada e recarregue a exibição de rolagem.
Como esse código difere do uso direto do Isolate?
- Essa abordagem requer menos código e fornece uma interface consistente para trabalhar com Isolates.
Como esse código difere do uso da função de computação do Flutter?
- O Isolate é reutilizado sempre que o aplicativo recebe a solicitação para descriptografar uma nova mensagem.
- O Isolate pode armazenar o estado, neste caso no caminho do diretório de documentos, para que não precise recalculá-lo no Isolate ou enviá-lo todas as vezes.
Ambas as coisas contribuem para um melhor desempenho.
Uma nota sobre o desempenho
Embora o Isolated_agents desbloqueie casos de uso de multiprocessamento mais complicados, isso é um pouco prejudicado pelo desempenho da API SendPort do Dart. Deve-se considerar o overhead dos payloads que são enviados entre os Agentes. Por esta razão, os seguintes métodos foram adicionados:
- O método Agent.read permite ler uma porção menor do estado mantido por um Agente com o parâmetro de consulta.
- O método Agent.exit permite ler o valor de um Agente morrendo em tempo constante.
O Dart tenta, na maioria dos casos, otimizar a comunicação de dados entre Isolados, mas infelizmente não é explícito sobre quando essas otimizações acontecem. Espero que, em algum momento, o Dart possa ter uma semântica de movimento que nos permita afirmar a transmissão de dados em tempo constante entre os isolados, mas parece improvável que isso aconteça tão cedo.