Dart program not exiting - dart

I'm having trouble tracking down why my dart program isn't terminating. I'm pretty sure it's something to do with either isolates, or stream controllers, and making sure they are closed, but I can't work out what the problem is.
On all of my StreamControllers i'm calling await streamControllerName.close();, but I think there is an isolate closing mechanism that I don't know about. Would there be any reason why the event loop isn't finishing and so the program isn't exiting? Difficult to give more details as the code is quite long.

Dart programs stops when the main isolate has nothing left to do (so no code is executing and no events on the event or microtask queues) and nothing is subscribed to which can spawn events into the main isolate.
Besides Timer which can spawn delayed events, we have ReceivePort which allow us to send events into an isolate from another isolate. It should be noted that Dart does not keep track of the relationship between a ReceivePort and SendPort instances pointing to that ReceivePort.
This means that Dart does not know when a ReceivePort is no longer "in use" and it is therefore up to the developer to ensure closing any open ReceivePort by calling the close() method on each instance (notice that SendPort does not have any close() method) when they are no longer in use.
As long as there are any open ReceivePort in the main isolate, the program will continue running since an event might be spawned from one of these ReceivePort instances.

Related

Correctly killing newly spawned isolates

I am aware of the fact that when both microtask and event queues of an isolate are empty, the isolate is killed. However, I'm not able to find a reference on the documentation of how a worker isolate can be killed under certain circumstances.
Context
Let's make this example:
Future<void> main() {
final receivePort = ReceivePort();
final worker = await Isolate.spawn<SendPort>((_) {}, receivePort.sendPort);
await runMyProgram(receivePort, worker);
}
Here the main isolate is creating a new one (worker) and then the program starts doing stuff.
Question
How do I manually kill the newly spawned isolate when it's not needed anymore? I wasn't able to explicitly find this information on the documentation so I am kind of guessing. Do I have to do this?
receivePort.close();
worker.kill();
Or is it enough to just close the port, like this?
receivePort.close();
Note
I thought about this. If the worker isolate has both queues (microtask and event) empty and I close the receive port, it should be killed automatically. If this is the case, calling receivePort.close() should be enough!
If you want to make sure the child isolate shuts down, even if it's in the middle of doing something, you'll want to call Isolate.kill().
However, as you've already pointed out, an isolate will exit on its own if it has no more events to process and it holds no open ports (e.g., timers, open sockets, isolate ports, etc). For most cases, this is the ideal way to dispose of an isolate when it's no longer used since it eliminates the risk of killing the isolate while it's in the middle of doing something important.
Assuming your child isolate is good about cleaning up its own open ports when it's done doing what it needs to do, receivePort.close() should be enough for you to let it shut down.
You can kill an isolate from the outside, using the Isolate.kill method on an Isolate object representing that isolate.
(That's why you should be careful about giving away such isolate objects, and why you can create an isolate object without the "kill" capability, that you can more safely pass around.)
You can immediately kill an isolate from the inside using the static Isolate.exit.
Or using Isolate.current.kill. It's like Process.exit, but only for a single isolate.
Or you can make sure you have closed every open receive port in the isolate, and stopped doing anything.
That's the usual approach, but it can fail if you run code provided by others in your isolate. They might open receive ports or start periodic timers which run forever, and that you know nothing about.
(You can try to contain that code in a Zone where you control timers, but that won't stop them from creating receive ports, and they can always access Zone.root directly to leave the zone you put them in.)
Or someone might have Isolate.pauseed your isolate, so the worker code won't run.
If I wanted to be absolutely certain that an isolate is killed,
I'd start out by communicating with my own code running in that isolate (the port receiving worker instructions) and tell it to shut down nicely, as a part of the protocol I am already using to communicate.
The worker code can choose to use Isolate.exit when it's done, or just close all its own resources and hope it's enough. I'd probably tend to use Isolate.exit, but only after waiting for existing worker tasks getting done.
Such a worker task might be hanging (waiting for a future which will never complete). Or it might be live-locking everything by being stuck in a while (true){..can't stop, won't stop!..}. In that case, the waiting should have a timeout.
Because of that, I'd also listen for the isolate to shut down, using Isolate.addOnExitHandler, and start a timer for some reasonable duration, and if I haven't received an "on exit" notification before the timer runs out, or some feedback on the worker shutdown request telling me that things are fine, I'd escalate to isolate.kill(priority: Isolate.immediate); which can kill even a while (true) ... loop.

How to understand dart async operation?

As we know, dart is a single-threaded language. So according to the document, we can use Futrure/Stream to implement a async opetation. It sends the time-consuming operation to the Event Queue.
What confused me is where the Event Queue working on. It is working on the dart threat? if yes, it will block the app.
Another question is Event Queue a FIFO queue. If i have two opertion, one is a 1mins needed networking request, the other is a click event. The two operation will send to the Event Queue.
So if the click event will blocked by the networking request? Because the queue is a FIFO queue?
So where is the event queue working on?
Thank you very much!
One thing to note is that asynchronous and multithreading are two different things. Dart uses Futures and async/await to achieve asynchronicity, but Dart is still inherently a single-threaded language.
The way it works is when a Future is created (either manually or via calling an async method), that process is added to an event queue, as you read. Then, in the middle of all the synchronous execution, whenever there is a lull, the event queue can take priority. It can then go through the processes and figure out if any of the Futures have been completed. If so, the result is passed along to any other asynchronous processes that are waiting on that resource, if any.
This also means that, yes, if your program hangs in the middle of an asynchronous operation (with the easy example of an endless loop via while (true) {}), it will freeze the entire program, including the synchronous code and other asynchronous processes still waiting to resolve (even if the conditions allowing them to resolve have already occurred).
However, in your case, this won't be an issue. If you fire an asynchronous process in the form of a network request followed by another in the form of a "click event" (not sure what you're referring to, but I'll assume it's asynchronous as well), they will both be added to the event queue in that order. But if the click event resolves before the network request, the event queue will merely recognize that the network request Future has not yet resolved and will move on to the click event that has.
As a side note, it's worth noting that Dart does have a multi-threading capability, albeit in a fairly roundabout way. Dart has something called an Isolate, which isn't a thread but a completely separate child program. This means that the Isolate cannot access any of the same data in memory as the root program itself. However, data can be passed between the two using SendPorts and ReceivePorts. This makes using Isolates slightly more complicated than threads, but it also means that, if no memory is shared, it virtually eliminates race conditions based on which thread accesses the memory first.

Flutter Isolate vs Future

I might have the wrong idea of Isolate and Future. Please help me to clear it up. Here is my understanding of both subjects.
Isolate: Isolates run code in its own event loop, and each event may run smaller tasks in a nested microtask queue.
Future: A Future is used to represent a potential value, or error, that will be available at some time in the future.
My confusions are:
The doc says Isolate has it own loop? I feel like having its own event queue makes more sense to me, am I wrong?
Is future running asynchronously on the main Isolate? I'm assuming future task actually got placed at the end of event queue so if it will be execute by loop in the future. Correct me if I'm wrong.
Why use Isolate when there is future? I saw some examples using Isolate for some heavy task instead of Future. But why? It only makes sense to me when future execute asynchronously on the main isolate queue.
A Future is a handle that allows you to get notified when async execution is completed.
Async execution uses the event queue and code is executed concurrently within the same thread.
https://webdev.dartlang.org/articles/performance/event-loop
Dart code is by default executed in the root isolate.
You can start up additional isolates that usually run on another thread.
An isolate can be either loaded from the same Dart code the root isolate was started with (with a different entry-point than main() https://api.dartlang.org/stable/2.0.0/dart-isolate/Isolate/spawn.html) or with different Dart code (loaded from some Dart file or URL https://api.dartlang.org/stable/2.0.0/dart-isolate/Isolate/spawnUri.html).
Isolates don't share any state and can only communicate using message passing (SendPort/ReceivePort). Each isolate has its own event queue.
https://webdev.dartlang.org/articles/performance/event-loop
An Isolate runs Dart code on a single thread. Synchronous code like
print('hello');
is run immediately and can't be interrupted.
An Isolate also has an Event Loop that it uses to schedule asynchronous tasks on. Asynchronous doesn't mean that these tasks are run on a separate thread. They are still run on the same thread. Asynchronous just means that they are scheduled for later.
The Event Loop runs the tasks that are scheduled in what is called an Event Queue. You can put a task in the Event Queue by creating a future like this:
Future(() => print(hello));
The print(hello) task will get run when the other tasks ahead of it in the Event Queue have finished. All of this is happening on the same thread, that is, the same Isolate.
Some tasks don't get added to the Event Queue right away, for example
Future.delayed(Duration(seconds: 1), () => print('hello'));
which only gets added to the queue after a delay of one second.
So far everything I've been talking about gets done on the same thread, the same Isolate. Some work may actually get done on a different thread, though, like IO operations. The underlying framework takes care of that. If something expensive like reading from disk were done on the main Isolate thread then it would block the app until it finished. When the IO operation finishes the future completes and the update with the result is added to the Event Queue.
When you need to do CPU intensive operations yourself, you should run them on another isolate so that it doesn't cause jank in your app. The compute property is good for this. You still use a future, but this time the future is returning the result from a different Isolate.
Further study
Futures - Isolates - Event Loop
Dart asynchronous programming: Isolates and event loops
Are Futures in Dart threads?
The Event Loop and Dart
Flutter/Dart non-blocking demystify
The Engine architecture
Single Thread Dart, What? — Part 1
Single Thread Dart, What? — Part 2
Flutter Threading: Isolates, Future, Async And Await
The Fundamentals of Zones, Microtasks and Event Loops in the Dart Programming Language
An introduction to the dart:io library
What thread / isolate does flutter run IO operations on?
In one sentence we could say,
Isolates: Dart is single-threaded but it is capable of doing multi-threading stuff using Isolates (many processes).
Future: Future is a result which is returned when dart has finished an asynchronous work. The work is generally done in that single-thread.
Isolate could be compared to Thread even if dart is not multithreaded. It has it's own memory and event loop indeed, when Futures shares the same memory
Dart is able to spawn standalone processes, called Isolates (web workers in dart2js), which do not share memory when the main program, but are able to asynchronously, in another process (effectively a thread of sorts) is able to do computations without blocking the main thread.
A Future is run inside the Isolate that called it, not necesserally the main isolate.
I recommend this article which has better explanation than me.
TLDR: https://medium.com/flutter-community/isolates-in-flutter-a0dd7a18b7f6
Let's understand async-await first and then go into isolates.
void main() async {
// Read some data.
final fileData = await _readFileAsync();
final jsonData = jsonDecode(fileData);
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
Future<String> _readFileAsync() async {
final file = File(filename);
final contents = await file.readAsString();
return contents.trim();
}
We want to read some data from a file and then decode that JSON and print the JSON Keys length. We don’t need to go into the implementation details here but can take the help of the image below to understand how it works.
When we click on this button Place Bid, it sends a request to _readFileAsync, all of which is dart code that we wrote. But this function _readFileAsync, executes code using Dart Virtual Machine/OS to perform the I/O operation which in itself is a different thread, the I/O thread.
This means, the code in the main function runs inside the main isolate. When the code reaches the _readFileAsync, it transfers the code execution to I/O thread and the Main Isolate waits until the code is completely executed or an error occurs. This is what await keyword does.
Now, once the contents of the files are read, the control returns back to the main isolate and we start parsing the String data as JSON and print the number of keys. This is pretty straight forward. But let’s suppose, the JSON parsing was a very big operation, considering a very huge JSON and we start manipulating the data to conform to our needs. Then this work is happening on the Main Isolate. At this point of time, the UI could hang, making our users fustrated.
Now let's get back to isolates.
Dart uses Isolate model for concurrency. Isolate is nothing but a wrapper around thread. But threads, by definition, can share memory which might be easy for the developer but makes code prone to race conditions and locks. Isolates on the other hand cannot share memory and instead rely on message passing mechanism to talk with each other.
Using isolates, Dart code can perform multiple independent tasks at once, using additional cores if they’re available. Each Isolate has its own memory and a single thread running an event loop.
Hope this help solve someone's doubt.

Why does creating a single ReceiverPort cause the Dart VM to hang?

e.g.:
import 'dart:isolate';
void main() { var p = new ReceivePort(); }
This will make the whole VM hang until I Ctrl-C it. Why is this?
Dart's main function operates a bit differently than other platforms. It's more of an 'init' than anything else; it can exit and the application may continue running. A Dart VM application stays alive if it is listening for events. This generally means one or more open Streams. A ReceivePort is a Stream. Closing this stream would terminate the application.
You can verify this by running this script with dart --observe script.dart and viewing the application in Observatory. You'll notice that you have one isolate and it is 'idle' - this means there are ports open that are waiting for messages. You can click 'see ports' in the isolate panel and the ReceivePort will be the only item in the list. In general, if you are hanging and you can't figure out why, fire up Observatory and check which ports are open.
A Dart isolate stays alive as long as it has something to do.
If you start an asynchronous computation in main, then the isolate keeps running after main completes, waiting for the computation to complete.
When there are no further computations running, the program ends.
A ReceivePort is a port that can receive data from somewhere else. As long as one of those are open, the isolate doesn't know that it's not done. A new event might arrive on the ReceivePort to trigger more computation. The isolate itself doesn't know whether anyone has a SendPort that can send it data, it just assumes that it's possible.
So, a ReceivePort keeps the isolate, and the program, alive because the program doesn't know for sure that it's not done computing yet. That's a good thing. You can create a new isolate and have it wait for commands on a ReceivePort without that isolate shutting down the first time it's idle.
It does mean that you need to close your ports when you are done.
I believe the thread (or webworker) started by the ReceivePort is still alive, and needs to be explicitly shut down before the whole app can exit. Try adding p.close() and if that exits, that explains it.

How to terminate a long running isolate

I am trying to understand how I shall port my Java chess engine to dart.
So I have understood that I should use Isolates and/or Futures to run my engine in parallell with the GUI but how can I force the engine to terminate the search.
In java I just set some boolean that where shared between the engine thread and the gui thread.
You should send a message to the isolate, telling it to stop. You can simply do something like:
port.send('STOP');
To be clear, isolates and futures are two different things, and you use them differently.
Use an isolate when you want some code to truly run concurrently, in a separate "isolated memory heap". An isolate is like a mini program, running separately from your main program. You send isolates messages, and you can receive messages from isolates.
Use a future when you want to be notified when a value is available later. "Later" is defined as "a future tick in the event loop". Each isolate has its own event loop. It's important to understand that just asking a Future to run a function doesn't make the function run in parallel. It just puts the function onto the event loop to be run "later".
Answering the implied question 'how can I get a long running task in an isolate to cease running?' rather than more explicitly asked 'how can I cause an isolate to terminate, release it's resources and generally cease to be?'
Break the long running task up into smaller, shorter running units.
Execute each unit with a Future. Chain futures as appropriate.
Provide a flag that each unit should check before executing its logic. If the flag is set, bail.
Listen for a 'stop' message and set the flag if/when received.
Splitting the main processing task up into Futures allows processing of the stop message to get onto the event queue ahead of units of processing of the main task.
There is now iso.Isolate.kill()
WARNING: This method is experimental and not handled on every platform yet.

Resources