Flutter Threading: Isolates, Future, Async And Await

Flutter applications start with a single execution process to manage executing code. Inside this process you will find different ways that the process handles multiple pieces of code executing at the same time.

Isolates

When Dart starts, there will be one main Isolate(Thread). This is the main executing thread of the application, also referred to as the UI Thread. Isolates are:

  • Dart’s version of Threads.
  • Do no share memory between each other.
  • Uses Ports and Messages to communicate between them.
  • May use another processor core if available.
  • Runs code in parallel.

In simple Flutter apps you will only ever use one Isolate, and your app will run smoothly. Isolates are great if you have a long running task that you want to process, while allowing the rest of your app to run as unencumbered as possible.

Event Loops & Microtasks

Flutter apps, aren’t as simple as a single step by step line of executing code. You have user clicks, timers, keyboard input and more, all wanting to process code. If there is only one thread, then how do these events and code get processed? The answer is with the Event and Microtask Queue.

The event queue is the pathway for external sources to pass requests for processing. Each one is dequeued as received and executed. It is a First In First Out (FIFO) queue. A Microtask queue is for code that needs to be executed in an asynchronous manner but isn’t from an external source. This might occur after an event has triggered code that needs to be run before returning control to the event loop.

 

The Microtask queue has complete priority, and the event loop will not dequeue from the Event queue until the the Microtask queue is empty. It will always check the Microtask queue first, before dequeuing off the Event queue. We can test with some simple code.

If you run this via the click of a button in Flutter.

import 'dart:io';

  String _data = "";
  int clickCount = 0;

  Future<void> _buttonClick() async {
    var data = _data + "Started $clickCount: ${DateTime.now().toString()}\n";
    sleep(Duration(seconds: 2));  
    data += "End $clickCount: ${DateTime.now().toString()}\n"; 
    clickCount += 1;
    setState(() {
      _data = data;
    });
  }

You get the following output, after 3 rapid consecutive clicks. Considering my badly reused state variable there, we can see that it waited until the code ran, before the next click event was processed.

Started 0: 2019-01-14 10:47:27.769285 
End 0: 2019-01-14 10:47:29.779730
Started 1: 2019-01-14 10:47:29.784756 
End 1: 2019-01-14 10:47:31.785403
Started 2: 2019-01-14 10:47:31.789822 
End 2: 2019-01-14 10:47:33.792032

Note: You need to be wary of async tasks. As soon as you actually use an await, as for example await Future.delayed(new Duration(seconds: 2)); the event loop will now become active again. Because an await allows concurrency in the same thread, it must allow the event loop to continue, hence the result of using an await instead of sleep will look more like this.

Started 0: 2019-01-14 10:47:27.769285 
End 2: 2019-01-14 10:47:33.792032

I will go more into async and await further below.

Create New Isolate

If you want to create another thread, that has its own event queue, you need to create a new Isolate. You can do this using the spawn method, and send and received messages as shown. Remember that memory is not shared, hence you can’t use variables to pass data back and forth. You can however send complex objects as messages.

import 'dart:isolate';

void mainApp() async {

  var receivePort = new ReceivePort();
  await Isolate.spawn(entryPoint, receivePort.sendPort);

  // Receive the SendPort from the Isolate
  SendPort sendPort = await receivePort.first;

  // Send a message to the Isolate
  sendPort.send("hello");
}

// Entry point for your Isolate
entryPoint(SendPort sendPort) async {

  // Open the ReceivePort to listen for incoming messages (optional)
  var port = new ReceivePort();

  // Send messages to other Isolates
  sendPort.send(port.sendPort);

  // Listen for messages (optional)
  await for (var data in port) {
    // `data` is the message received.      
  }
}

You won’t use Isolates usually in many Flutter applications, you are more likely to remain in one Isolate but want asynchronous execution. Asynchronous communication is doable through the async and await keywords as we will go through next.

Future with Async & Await

async and await are keywords you can use in Dart, against a Future.  When running async code:

  • It runs in the same Isolate(Thread) that started it.
  • Runs concurrently (not parallel) at the same time as other code, in the same Isolate(Thread).

This is important, in that it does not block other code from running in the same thread. Particularly important when you are in the main UI Thread. This will generally help keep your UI smooth, while dealing with many events occurring in your code.

Future

A Future represents a task that will complete or fail sometime in the future, and you will be notified when. In this simple example, we have a Future, that returns a value. It does so instantly (this is just an example). When you call myFunction(), it adds it to the Event queue, to be completed. It will continue with all the synchronous code in main() function first someOtherFunction(), then start to process each asynchronous call from the Event queue.

Exception: If you use Future.delayed or Future.value it actually puts it in the Microtask queue.

import 'dart:async';

Future myFunction() => new Future.value('Hello');

void main() {
  myFunction().then((value) => debugPrint(value)); // Added to Microtask queue
  myFunction().then((value) => debugPrint(value)); // Added to Microtask queue
  someOtherFunction(); // Runs this code now
  // Now starts running tasks from Microtask queue.
}

If you want to wait for the function to finish you need to use await.

Async & Await

This simple code, will now stop the synchronous execution, while it waits for a result from an asynchronous task.

void mainTest() async {
  debugPrint(await myFunction()); // Waits for completion
  debugPrint(await myFunction()); // Waits for completion
  someOtherFunction(); // Runs this code
}
Share on
© 2019 Adam Pedley