Isolates
Informações sobre como escrever isolates em Dart.
Esta página discute alguns exemplos que usam a API Isolate para implementar
isolates.
Você deve usar isolates sempre que sua aplicação estiver lidando com computações que são grandes o suficiente para bloquear temporariamente outras computações. O exemplo mais comum é em aplicações Flutter, quando você precisa realizar grandes computações que de outra forma poderiam causar que a UI se torne não responsiva.
Não há regras sobre quando você deve usar isolates, mas aqui estão algumas situações adicionais onde eles podem ser úteis:
- Analisar e decodificar blobs JSON excepcionalmente grandes.
- Processar e comprimir fotos, áudio e vídeo.
- Converter arquivos de áudio e vídeo.
- Realizar buscas e filtragens complexas em listas grandes ou dentro de sistemas de arquivos.
- Realizar I/O, como comunicar-se com um banco de dados.
- Lidar com um grande volume de requisições de rede.
Implementando um worker isolate simples
#
Estes exemplos implementam um isolate principal
que gera um worker isolate simples.
Isolate.run()
simplifica os passos por trás da
configuração e gerenciamento de worker isolates:
- Gera (inicia e cria) um isolate.
- Executa uma função no isolate gerado.
- Captura o resultado.
- Retorna o resultado para o isolate principal.
- Termina o isolate uma vez que o trabalho está completo.
- Verifica, captura e lança exceções e erros de volta ao isolate principal.
Executando um método existente em um novo isolate
#-
Chame
run()para gerar um novo isolate (um background worker), diretamente no main isolate enquantomain()aguarda o resultado:
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final jsonData = await Isolate.run(_readAndParseJson);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
-
Passe ao worker isolate a função que você quer que ele execute
como seu primeiro argumento. Neste exemplo, é a função existente
_readAndParseJson():
Future<Map<String, dynamic>> _readAndParseJson() async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
return jsonData;
}
-
Isolate.run()pega o resultado que_readAndParseJson()retorna e envia o valor de volta ao isolate principal, encerrando o worker isolate. -
O worker isolate transfere a memória que contém o resultado para o isolate principal. Ele não copia os dados. O worker isolate realiza uma passagem de verificação para garantir que os objetos podem ser transferidos.
_readAndParseJson() é uma função
assíncrona existente que poderia facilmente
executar diretamente no isolate principal.
Usar Isolate.run() para executá-la em vez disso habilita concorrência.
O worker isolate abstrai completamente as computações
de _readAndParseJson(). Ele pode completar sem bloquear o isolate principal.
O resultado de Isolate.run() é sempre uma Future,
porque o código no isolate principal continua a executar.
Se a computação que o worker isolate executa
é síncrona ou assíncrona não impacta o
isolate principal, porque está executando de forma concorrente de qualquer maneira.
Para o programa completo, confira a amostra send_and_receive.dart.
Enviando closures com isolates
#
Você também pode criar um worker isolate simples com run() usando um
literal de função, ou closure, diretamente no isolate principal.
const String filename = 'with_keys.json';
void main() async {
// Read some data.
final jsonData = await Isolate.run(() async {
final fileData = await File(filename).readAsString();
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
return jsonData;
});
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
Este exemplo realiza o mesmo que o anterior. Um novo isolate é gerado, computa algo e envia de volta o resultado.
No entanto, agora o isolate envia uma closure.
Closures são menos limitadas do que funções nomeadas típicas,
tanto em como elas funcionam quanto em como são escritas no código.
Neste exemplo, Isolate.run() executa o que parece ser código local,
de forma concorrente. Nesse sentido, você pode imaginar run() funcionar como um
operador de fluxo de controle para "executar em paralelo".
Enviando múltiplas mensagens entre isolates com ports
#
Isolates de curta duração são convenientes de usar,
mas exigem sobrecarga de desempenho para gerar novos isolates
e copiar objetos de um isolate para outro.
Se seu código depende de executar repetidamente a mesma computação
usando Isolate.run, você pode melhorar o desempenho criando
isolates de longa duração que não saem imediatamente.
Para fazer isso, você pode usar algumas das APIs de isolate de baixo nível que
Isolate.run abstrai:
Esta seção revisa os passos necessários para estabelecer comunicação bidirecional entre um isolate recém-gerado e o main isolate. O primeiro exemplo, Ports básicos, introduz o processo em um alto nível. O segundo exemplo, Ports robustos, gradualmente adiciona funcionalidade mais prática e do mundo real ao primeiro.
ReceivePort e SendPort
#
Configurar comunicação de longa duração entre isolates requer
duas classes (além de Isolate): ReceivePort e SendPort.
Esses ports são a única maneira de isolates se comunicarem uns com os outros.
Um ReceivePort é um objeto que manipula mensagens que são enviadas de outros
isolates. Essas mensagens são enviadas via um SendPort.
Ports se comportam de forma similar a objetos Stream
(na verdade, receive ports implementam Stream!)
Você pode pensar em um SendPort e ReceivePort como
o StreamController e listeners de Stream, respectivamente.
Um SendPort é como um StreamController porque você "adiciona" mensagens a eles
com o método SendPort.send(), e essas mensagens são manipuladas por um listener,
neste caso o ReceivePort. O ReceivePort então manipula as mensagens que
recebe passando-as como argumentos para um callback que você fornece.
Configurando ports
#
Um isolate recém-gerado só tem as informações que recebe através da
chamada Isolate.spawn. Se você precisa que o isolate principal continue a comunicar-se
com um isolate gerado após sua criação inicial, você deve configurar um
canal de comunicação onde o isolate gerado possa enviar mensagens ao
isolate principal. Isolates só podem se comunicar via passagem de mensagens.
Eles não podem "ver" dentro da memória uns dos outros,
que é de onde vem o nome "isolate".
Para configurar essa comunicação bidirecional, primeiro crie um ReceivePort
no isolate principal, então passe seu SendPort
como argumento ao
novo isolate ao gerá-lo com Isolate.spawn.
O novo isolate então cria seu próprio ReceivePort, e envia seu
SendPort
de volta no SendPort que foi passado pelo isolate principal.
O isolate principal recebe este SendPort, e
agora ambos os lados têm um canal aberto para enviar e receber mensagens.
-
Crie um
ReceivePortno isolate principal. OSendPorté criado automaticamente como uma propriedade noReceivePort. - Gere o worker isolate com
Isolate.spawn() -
Passe uma referência a
ReceivePort.sendPortcomo a primeira mensagem ao worker isolate. - Crie outro novo
ReceivePortno worker isolate. -
Passe uma referência ao
ReceivePort.sendPortdo worker isolate como a primeira mensagem de volta ao isolate principal.
Junto com criar os ports e configurar comunicação, você também precisará
dizer aos ports o que fazer quando eles receberem mensagens. Isso é feito usando
o método listen em cada respectivo ReceivePort.
- Envie uma mensagem via a referência do isolate principal ao
SendPortdo worker isolate. -
Receba e manipule a mensagem via um listener no
ReceivePortdo worker isolate. Isto é onde a computação que você quer mover do isolate principal é executada. -
Envie uma mensagem de retorno via a referência do worker isolate ao
SendPortdo isolate principal. - Receba a mensagem via um listener no
ReceivePortdo isolate principal.
Exemplo de ports básicos
#Este exemplo demonstra como você pode configurar um worker isolate de longa duração com comunicação bidirecional entre ele e o isolate principal. O código usa o exemplo de enviar texto JSON para um novo isolate, onde o JSON será analisado e decodificado, antes de ser enviado de volta ao isolate principal.
Passo 1: Definir a classe worker
#Primeiro, crie uma classe para seu worker isolate em segundo plano. Esta classe contém toda a funcionalidade que você precisa para:
- Gerar um isolate.
- Enviar mensagens para esse isolate.
- Fazer o isolate decodificar algum JSON.
- Enviar o JSON decodificado de volta ao isolate principal.
A classe expõe dois métodos públicos: um que gera o worker isolate, e um que manipula o envio de mensagens para esse worker isolate.
As seções restantes neste exemplo mostrarão como preencher os métodos da classe, um por um.
class Worker {
Future<void> spawn() async {
// TODO: Add functionality to spawn a worker isolate.
}
void _handleResponsesFromIsolate(dynamic message) {
// TODO: Handle messages sent back from the worker isolate.
}
static void _startRemoteIsolate(SendPort port) {
// TODO: Define code that should be executed on the worker isolate.
}
Future<void> parseJson(String message) async {
// TODO: Define a public method that can
// be used to send messages to the worker isolate.
}
}
Passo 2: Gerar um worker isolate
#
O método Worker.spawn é onde você agrupará o código para criar o
worker isolate e garantir que ele possa receber e enviar mensagens.
-
Primeiro, crie um
ReceivePort. Isso permite que o isolate principal receba mensagens enviadas do worker isolate recém-gerado. -
Em seguida, adicione um listener ao receive port para manipular mensagens que o worker isolate
enviará de volta. O callback passado ao
listener,
_handleResponsesFromIsolate, será coberto no passo 4. -
Finalmente, gere o worker isolate com
Isolate.spawn. Ele espera dois argumentos: uma função a ser executada no worker isolate (coberta no passo 3), e a propriedadesendPortdo receive port.
Future<void> spawn() async {
final receivePort = ReceivePort();
receivePort.listen(_handleResponsesFromIsolate);
await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}
O argumento receivePort.sendPort será passado ao
callback (_startRemoteIsolate) como um argumento quando for chamado no
worker isolate. Este é o primeiro passo para garantir que o worker isolate tenha uma
maneira de enviar mensagens de volta ao isolate principal.
Passo 3: Executar código no worker isolate
#
Neste passo, você define o método _startRemoteIsolate que é enviado ao
worker isolate para ser executado quando ele é gerado. Este método é como o método "main"
para o worker isolate.
-
Primeiro, crie outro novo
ReceivePort. Este port recebe mensagens futuras do isolate principal. - Em seguida, envie o
SendPortdesse port de volta ao isolate principal. -
Finalmente, adicione um listener ao novo
ReceivePort. Este listener manipula mensagens que o isolate principal envia ao worker isolate.
static void _startRemoteIsolate(SendPort port) {
final receivePort = ReceivePort();
port.send(receivePort.sendPort);
receivePort.listen((dynamic message) async {
if (message is String) {
final transformed = jsonDecode(message);
port.send(transformed);
}
});
}
O listener no ReceivePort do worker decodifica o JSON passado do isolate principal,
e então envia o JSON decodificado de volta ao isolate principal.
Este listener é o ponto de entrada para mensagens enviadas do isolate principal ao worker isolate. Esta é a única chance que você tem de dizer ao worker isolate que código executar no futuro.
Passo 4: Manipular mensagens no isolate principal
#
Finalmente, você precisa dizer ao isolate principal como manipular mensagens enviadas do
worker isolate de volta ao isolate principal. Para fazer isso, você precisa preencher
o método _handleResponsesFromIsolate. Lembre-se que este método é passado ao
método receivePort.listen, conforme descrito
no passo 2:
Future<void> spawn() async {
final receivePort = ReceivePort();
receivePort.listen(_handleResponsesFromIsolate);
await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}
Lembre-se também que você enviou um SendPort de volta ao isolate principal
no passo 3. Este método manipula o
recebimento desse SendPort, bem como o recebimento de mensagens futuras (que serão
JSON decodificado).
-
Primeiro, verifique se a mensagem é um
SendPort. Se for, atribua esse port à propriedade_sendPortda classe para que possa ser usado para enviar mensagens depois. -
Em seguida, verifique se a mensagem é do tipo
Map<String, dynamic>, o tipo esperado de JSON decodificado. Se for, manipule essa mensagem com sua lógica específica da aplicação. Neste exemplo, a mensagem é impressa.
void _handleResponsesFromIsolate(dynamic message) {
if (message is SendPort) {
_sendPort = message;
_isolateReady.complete();
} else if (message is Map<String, dynamic>) {
print(message);
}
}
Passo 5: Adicionar um completer para garantir que seu isolate esteja configurado
#
Para completar a classe, defina um método público chamado parseJson, que é
responsável por enviar mensagens ao worker isolate. Ele também precisa garantir
que mensagens possam ser enviadas antes do isolate estar totalmente configurado.
Para lidar com isso, use um Completer.
-
Primeiro, adicione uma propriedade de nível de classe chamada
Completere nomeie-a_isolateReady. -
Em seguida, adicione uma chamada a
complete()no completer no método_handleResponsesFromIsolate(criado no passo 4) se a mensagem for umSendPort. -
Finalmente, no método
parseJson, adicioneawait _isolateReady.futureantes de adicionar_sendPort.send. Isso garante que nenhuma mensagem possa ser enviada ao worker isolate até que ele seja gerado e tenha enviado seuSendPortde volta ao isolate principal.
Future<void> parseJson(String message) async {
await _isolateReady.future;
_sendPort.send(message);
}
Exemplo completo
#Expanda para ver o exemplo completo
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
void main() async {
final worker = Worker();
await worker.spawn();
await worker.parseJson('{"key":"value"}');
}
class Worker {
late SendPort _sendPort;
final Completer<void> _isolateReady = Completer.sync();
Future<void> spawn() async {
final receivePort = ReceivePort();
receivePort.listen(_handleResponsesFromIsolate);
await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort);
}
void _handleResponsesFromIsolate(dynamic message) {
if (message is SendPort) {
_sendPort = message;
_isolateReady.complete();
} else if (message is Map<String, dynamic>) {
print(message);
}
}
static void _startRemoteIsolate(SendPort port) {
final receivePort = ReceivePort();
port.send(receivePort.sendPort);
receivePort.listen((dynamic message) async {
if (message is String) {
final transformed = jsonDecode(message);
port.send(transformed);
}
});
}
Future<void> parseJson(String message) async {
await _isolateReady.future;
_sendPort.send(message);
}
}
Exemplo de ports robustos
#O exemplo anterior explicou os blocos de construção básicos necessários para configurar um isolate de longa duração com comunicação bidirecional. Como mencionado, esse exemplo carece de alguns recursos importantes, como tratamento de erros, a capacidade de fechar os ports quando eles não estão mais em uso, e inconsistências em torno da ordenação de mensagens em algumas situações.
Este exemplo expande as informações do primeiro exemplo criando um worker isolate de longa duração que tem esses recursos adicionais e mais, e segue melhores padrões de design. Embora este código tenha semelhanças com o primeiro exemplo, não é uma extensão desse exemplo.
Passo 1: Definir a classe worker
#Primeiro, crie uma classe para seu worker isolate em segundo plano. Esta classe contém toda a funcionalidade que você precisa para:
- Gerar um isolate.
- Enviar mensagens para esse isolate.
- Fazer o isolate decodificar algum JSON.
- Enviar o JSON decodificado de volta ao isolate principal.
A classe expõe três métodos públicos: um que cria o worker isolate, um que manipula o envio de mensagens para esse worker isolate, e um que pode encerrar os ports quando eles não estão mais em uso.
class Worker {
final SendPort _commands;
final ReceivePort _responses;
Future<Object?> parseJson(String message) async {
// TODO: Ensure the port is still open.
_commands.send(message);
}
static Future<Worker> spawn() async {
// TODO: Add functionality to create a new Worker object with a
// connection to a spawned isolate.
throw UnimplementedError();
}
Worker._(this._responses, this._commands) {
// TODO: Initialize main isolate receive port listener.
}
void _handleResponsesFromIsolate(dynamic message) {
// TODO: Handle messages sent back from the worker isolate.
}
static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async {
// TODO: Handle messages sent back from the worker isolate.
}
static void _startRemoteIsolate(SendPort sp) {
// TODO: Initialize worker isolate's ports.
}
}
Passo 2: Criar um RawReceivePort no método Worker.spawn
#
Antes de gerar um isolate, você precisa criar um RawReceivePort, que é
um ReceivePort de nível mais baixo. Usar RawReceivePort
é um padrão preferido
porque permite que você separe sua lógica de inicialização de isolate da lógica que
manipula a passagem de mensagens no isolate.
No método Worker.spawn:
-
Primeiro, crie o
RawReceivePort. EsteReceivePorté apenas responsável por receber a mensagem inicial do worker isolate, que será umSendPort. -
Em seguida, crie um
Completerque indicará quando o isolate está pronto para receber mensagens. Quando isso completar, retornará um record com umReceivePorte umSendPort. -
Em seguida, defina a propriedade
RawReceivePort.handler. Esta propriedade é umaFunction?que se comporta comoReceivePort.listener. A função é chamada quando uma mensagem é recebida por este port. -
Dentro da função handler, chame
connection.complete(). Este método espera um record com umReceivePorte umSendPortcomo argumento. OSendPorté a mensagem inicial enviada do worker isolate, que será atribuído no próximo passo àSendPortde nível de classe chamada_commands. -
Então, crie um novo
ReceivePortcom o construtorReceivePort.fromRawReceivePort, e passe oinitPort.
class Worker {
final SendPort _commands;
final ReceivePort _responses;
static Future<Worker> spawn() async {
// Create a receive port and add its initial message handler.
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
}
}
Ao criar um RawReceivePort primeiro, e depois um ReceivePort, você será capaz
de adicionar um novo callback a ReceivePort.listen mais tarde. Por outro lado, se você fosse
criar um ReceivePort diretamente, você só poderia adicionar
um listener, porque ReceivePort implementa Stream, em vez
de BroadcastStream.
Efetivamente, isso permite que você separe sua lógica de inicialização de isolate da lógica que manipula o recebimento de mensagens após a configuração de comunicação estar completa. Este benefício se tornará mais óbvio conforme a lógica nos outros métodos crescer.
Passo 3: Gerar um worker isolate com Isolate.spawn
#
Este passo continua a preencher o método Worker.spawn. Você adicionará o código
necessário para gerar um isolate e retornar uma instância de Worker
desta classe.
Neste exemplo, a chamada a Isolate.spawn é envolvida em
um bloco try/catch, que garante que, se o isolate falhar ao iniciar,
o initPort será fechado, e o objeto Worker não será criado.
-
Primeiro, tente gerar um worker isolate em um bloco
try/catch. Se gerar um worker isolate falhar, feche o receive port que foi criado no passo anterior. O método passado aIsolate.spawnserá coberto em um passo posterior. -
Em seguida, aguarde o
connection.future, e desestruture o send port e receive port do record que ele retorna. -
Finalmente, retorne uma instância de
Workerchamando seu construtor privado, e passando os ports desse completer.
class Worker {
final SendPort _commands;
final ReceivePort _responses;
static Future<Worker> spawn() async {
// Create a receive port and add its initial message handler.
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
// Spawn the isolate.
try {
await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
} on Object {
initPort.close();
rethrow;
}
final (ReceivePort receivePort, SendPort sendPort) =
await connection.future;
return Worker._(receivePort, sendPort);
}
}
Note que neste exemplo (comparado ao exemplo anterior), Worker.spawn
atua como um construtor estático assíncrono para esta classe e é a única maneira de
criar uma instância de Worker. Isso simplifica a API, tornando o código que
cria uma instância de Worker mais limpo.
Passo 4: Completar o processo de configuração do isolate
#Neste passo, você completará o processo básico de configuração do isolate. Isso se correlaciona quase inteiramente ao exemplo anterior, e não há novos conceitos. Há uma pequena mudança em que o código é dividido em mais métodos, que é uma prática de design que o prepara para adicionar mais funcionalidade através do restante deste exemplo. Para uma explicação detalhada do processo básico de configuração de um isolate, veja o exemplo de ports básicos.
Primeiro, crie o construtor privado que é retornado do método Worker.spawn.
No corpo do construtor, adicione um listener ao receive port usado pelo
isolate principal, e passe um método ainda não definido a esse listener
chamado _handleResponsesFromIsolate.
class Worker {
final SendPort _commands;
final ReceivePort _responses;
Worker._(this._responses, this._commands) {
_responses.listen(_handleResponsesFromIsolate);
}
}
Em seguida, adicione o código a _startRemoteIsolate que é responsável por inicializar
os ports no worker
isolate. Lembre-se
que este
método foi passado a Isolate.spawn no método Worker.spawn, e ele receberá
o SendPort do isolate principal como um argumento.
- Crie um novo
ReceivePort. - Envie o
SendPortdesse port de volta ao isolate principal. -
Chame um novo método chamado
_handleCommandsToIsolate, e passe tanto o novoReceivePortquanto oSendPortdo isolate principal como argumentos.
static void _startRemoteIsolate(SendPort sendPort) {
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
_handleCommandsToIsolate(receivePort, sendPort);
}
Em seguida, adicione o método _handleCommandsToIsolate, que é responsável por
receber mensagens do isolate principal, decodificar json no worker isolate,
e enviar o json decodificado de volta como uma resposta.
- Primeiro, declare um listener no
ReceivePortdo worker isolate. -
Dentro do callback adicionado ao listener, tente decodificar o JSON passado
do isolate principal dentro de um bloco
try/catch. Se a decodificação for bem-sucedida, envie o JSON decodificado de volta ao isolate principal. -
Se houver um erro, envie de volta um
RemoteError.
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
try {
final jsonData = jsonDecode(message as String);
sendPort.send(jsonData);
} catch (e) {
sendPort.send(RemoteError(e.toString(), ''));
}
});
}
Em seguida, adicione o código para o método _handleResponsesFromIsolate.
-
Primeiro, verifique se a mensagem é um
RemoteError, caso em que você devethrowesse erro. - Caso contrário, imprima a mensagem. Em passos futuros, você atualizará este código para retornar mensagens em vez de imprimi-las.
void _handleResponsesFromIsolate(dynamic message) {
if (message is RemoteError) {
throw message;
} else {
print(message);
}
}
Finalmente, adicione o método parseJson, que é um método público que permite
código externo enviar JSON ao worker isolate para ser decodificado.
Future<Object?> parseJson(String message) async {
_commands.send(message);
}
Você atualizará este método no próximo passo.
Passo 5: Manipular múltiplas mensagens ao mesmo tempo
#Atualmente, se você enviar mensagens rapidamente ao worker isolate, o isolate enviará a resposta json decodificada na ordem em que elas completam, em vez de na ordem em que são enviadas. Você não tem maneira de determinar qual resposta corresponde a qual mensagem.
Neste passo, você corrigirá este problema dando a cada mensagem um id, e
usando objetos Completer para garantir que quando código externo chama
parseJson a
resposta que é retornada ao chamador é a resposta correta.
Primeiro, adicione duas propriedades de nível de classe a Worker:
Map<int, Completer<Object?>> _activeRequestsint _idCounter
class Worker {
final SendPort _commands;
final ReceivePort _responses;
final Map<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
// ···
}
O mapa _activeRequests associa uma mensagem enviada ao worker isolate
com um Completer. As chaves usadas em _activeRequests são tiradas
de _idCounter, que será incrementado conforme mais mensagens são enviadas.
Em seguida, atualize o método parseJson para criar completers antes de enviar
mensagens ao worker isolate.
- Primeiro crie um
Completer. -
Em seguida, incremente
_idCounter, para que cadaCompleterseja associado a um número único. -
Adicione uma entrada ao mapa
_activeRequestsem que a chave é o número atual de_idCounter, e o completer é o valor. -
Envie a mensagem ao worker isolate, junto com o id. Como você só pode
enviar um valor através do
SendPort, envolva o id e a mensagem em um record. - Finalmente, retorne a future do completer, que eventualmente conterá a resposta do worker isolate.
Future<Object?> parseJson(String message) async {
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, message));
return await completer.future;
}
Você também precisa atualizar _handleResponsesFromIsolate
e _handleCommandsToIsolate para manipular este sistema.
Em _handleCommandsToIsolate, você precisa considerar que a message seja um
record com dois valores, em vez de apenas o texto json. Faça isso desestruturando
os valores de message.
Então, após decodificar o json, atualize a chamada a sendPort.send para passar tanto
o id quanto o json decodificado de volta ao isolate principal, novamente usando um record.
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
final (int id, String jsonText) = message as (int, String); // New
try {
final jsonData = jsonDecode(jsonText);
sendPort.send((id, jsonData)); // Updated
} catch (e) {
sendPort.send((id, RemoteError(e.toString(), '')));
}
});
}
Finalmente, atualize o _handleResponsesFromIsolate.
- Primeiro, desestruture o id e a resposta do argumento message novamente.
-
Então, remova o completer que corresponde a esta requisição do
mapa
_activeRequests. -
Por último, em vez de lançar um erro ou imprimir o json decodificado, complete
o completer, passando a resposta. Quando isso completar, a resposta será
retornada ao código que chamou
parseJsonno isolate principal.
void _handleResponsesFromIsolate(dynamic message) {
final (int id, Object? response) = message as (int, Object?); // New
final completer = _activeRequests.remove(id)!; // New
if (response is RemoteError) {
completer.completeError(response); // Updated
} else {
completer.complete(response); // Updated
}
}
Passo 6: Adicionar funcionalidade para fechar os ports
#Quando o isolate não estiver mais sendo usado pelo seu código, você deve fechar os ports no isolate principal e no worker isolate.
- Primeiro, adicione um booleano de nível de classe que rastreia se os ports estão fechados.
-
Então, adicione o método
Worker.close. Dentro deste método:- Atualize
_closedpara ser true. - Envie uma mensagem final ao worker isolate.
Esta mensagem é uma
Stringque lê "shutdown", mas poderia ser qualquer objeto que você desejar. Você a usará no próximo trecho de código.
- Atualize
-
Finalmente, verifique se
_activeRequestsestá vazio. Se estiver, feche oReceivePortdo isolate principal chamado_responses.
class Worker {
bool _closed = false;
// ···
void close() {
if (!_closed) {
_closed = true;
_commands.send('shutdown');
if (_activeRequests.isEmpty) _responses.close();
print('--- port closed --- ');
}
}
}
-
Em seguida, você precisa manipular a mensagem "shutdown" no worker isolate. Adicione o
seguinte código ao método
_handleCommandsToIsolate. Este código verificará se a mensagem é umaStringque lê "shutdown". Se for, fechará oReceivePortdo worker isolate, e retornará.
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
// New if-block.
if (message == 'shutdown') {
receivePort.close();
return;
}
final (int id, String jsonText) = message as (int, String);
try {
final jsonData = jsonDecode(jsonText);
sendPort.send((id, jsonData));
} catch (e) {
sendPort.send((id, RemoteError(e.toString(), '')));
}
});
}
-
Finalmente, você deve adicionar código para verificar se os ports estão fechados antes de tentar
enviar mensagens. Adicione uma linha no método
Worker.parseJson.
Future<Object?> parseJson(String message) async {
if (_closed) throw StateError('Closed'); // New
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, message));
return await completer.future;
}
Exemplo completo
#Expanda aqui para ver o exemplo completo
import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
void main() async {
final worker = await Worker.spawn();
print(await worker.parseJson('{"key":"value"}'));
print(await worker.parseJson('"banana"'));
print(await worker.parseJson('[true, false, null, 1, "string"]'));
print(
await Future.wait([worker.parseJson('"yes"'), worker.parseJson('"no"')]),
);
worker.close();
}
class Worker {
final SendPort _commands;
final ReceivePort _responses;
final Map<int, Completer<Object?>> _activeRequests = {};
int _idCounter = 0;
bool _closed = false;
Future<Object?> parseJson(String message) async {
if (_closed) throw StateError('Closed');
final completer = Completer<Object?>.sync();
final id = _idCounter++;
_activeRequests[id] = completer;
_commands.send((id, message));
return await completer.future;
}
static Future<Worker> spawn() async {
// Create a receive port and add its initial message handler.
final initPort = RawReceivePort();
final connection = Completer<(ReceivePort, SendPort)>.sync();
initPort.handler = (initialMessage) {
final commandPort = initialMessage as SendPort;
connection.complete((
ReceivePort.fromRawReceivePort(initPort),
commandPort,
));
};
// Spawn the isolate.
try {
await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort));
} on Object {
initPort.close();
rethrow;
}
final (ReceivePort receivePort, SendPort sendPort) =
await connection.future;
return Worker._(receivePort, sendPort);
}
Worker._(this._responses, this._commands) {
_responses.listen(_handleResponsesFromIsolate);
}
void _handleResponsesFromIsolate(dynamic message) {
final (int id, Object? response) = message as (int, Object?);
final completer = _activeRequests.remove(id)!;
if (response is RemoteError) {
completer.completeError(response);
} else {
completer.complete(response);
}
if (_closed && _activeRequests.isEmpty) _responses.close();
}
static void _handleCommandsToIsolate(
ReceivePort receivePort,
SendPort sendPort,
) {
receivePort.listen((message) {
if (message == 'shutdown') {
receivePort.close();
return;
}
final (int id, String jsonText) = message as (int, String);
try {
final jsonData = jsonDecode(jsonText);
sendPort.send((id, jsonData));
} catch (e) {
sendPort.send((id, RemoteError(e.toString(), '')));
}
});
}
static void _startRemoteIsolate(SendPort sendPort) {
final receivePort = ReceivePort();
sendPort.send(receivePort.sendPort);
_handleCommandsToIsolate(receivePort, sendPort);
}
void close() {
if (!_closed) {
_closed = true;
_commands.send('shutdown');
if (_activeRequests.isEmpty) _responses.close();
print('--- port closed --- ');
}
}
}
Salvo indicação em contrário, a documentação neste site reflete Dart 3.13.7. Página atualizada pela última vez em 2026-11-12. Ver código-fonte ou relatar um problema.
Encontrou essa página sem tradução ou que precisa de correção? Abra uma issue e nos ajude a manter esse site em PT-BR para ajudar pessoas como você.