In development, we often encounter some time-consuming operations to complete, such as network requests, file reads, and so on; If our main thread is waiting for these time-consuming operations to complete, it blocks and cannot respond to other events, such as a user click; Obviously, we can’t do that!! How do you handle time-consuming operations?

Different languages have different ways of handling time-consuming operations.

  • Processing method 1: multithreading, such as Java and C++, we generally open a new Thread (Thread), complete these asynchronous operations in the new Thread, and then pass the data to the main Thread through inter-thread communication.
  • Method 2: Single thread + event loop. For example, JavaScript and Dart are all based on single thread + event loop to complete time-consuming operations.

How can a single thread perform time-consuming operations? ?

  • Blocking call: The current thread is suspended until the result of the call is returned, and the calling thread will not resume execution until it gets the result of the call.
  • Non-blocking calls: After the call is executed, the current thread does not stop executing. It just needs to wait for a while to check if the result is returned.

Many of the time-consuming operations in our development can be called on a non-blocking basis like this:

For example, the network request itself uses Socket communication, and Socket itself provides a SELECT model, which can work in a non-blocking manner. For file read/write IO operations, we can use the event-based callback mechanism provided by the operating system.

Event Loop and Event Queue

After the Flutter App is started, the main function is executed and the main ISOLATE is run

The ISOLATE contains an event loop and two event queues, event Queue and MicroTask queue. The event and MicroTask queues are similar to iOS source0 and source1. Source0 processes tasks with a higher priority in this thread, and source1 processes tasks transmitted by the system through port

Event Queue: Processes external events such as I/O events, drawing events, gesture events, and receiving other ISOLATE messages. Microtask Queue: You can add events to the ISOLATE. Events have a higher priority than event queue. You can add microtasks using scheduleMicrotask.

When we have some events, such as click events, IO events, network events, they are added to the event queue, and when it is found that the event queue is not empty, the event is fetched and executed.

RaisedButton( child: Text('Click me'), onPressed: () { final myFuture = http.get(''); myFuture.then((response) { if (response.statusCode == 200) { print('Success! '); }}); },)Copy the code

Asynchronous operation

2.1 the Future

Let’s start with a little example

import "dart:io";

main(List<String> args) {
  print("main function start");
  print("main function end");

String getNetworkData() {
  sleep(Duration(seconds: 3));
  return "network data";

Copy the code
Main function start // Wait 3 seconds Network data main function endCopy the code

If the main thread is stuck for 3 seconds, nothing can be done, which is obviously not possible, so let’s use Future to fix it

import "dart:io"; main(List<String> args) { print("main function start"); final result = getNetworkData(); Print (result); print(result); print(result); result.then((value){ print(value); }).catchError((e){ print(e); }). WhenComplete ((){print(' done '); }); print("main function end"); } Future<String> getNetworkData() { return Future<String>(() { sleep(Duration(seconds: 3)); return "network data"; // throw Exception(' error '); }); }Copy the code

The execution result

Main function start Instance of 'Future<String>' main function endCopy the code

Future through. Then handles the case of normal return of data, through.. CatchError to handle exceptions,.whenComplete to handle end-of-execution callbacks,

2.1.1 Future chain call
import "dart:io"; main(List<String> args) { print("main function start"); getNetworkData().then((value1) { print(value1); return "content data2"; }).then((value2) { print(value2); return "message data3"; }).then((value3) { print(value3); }); // We can continue here. print("main function end"); } Future<String> getNetworkData() { return Future<String>(() { sleep(Duration(seconds: 3)); return "network data1"; }); }Copy the code
2.1.2 Future Other apis

Future.value(value) generates a Future object whose state is completed, and inserts the. Then function as an event in the Event Queue to wait for the EventLoop to poll

main(List<String> args) { print("main function start"); Future.value(" hahaha "). Then (value) {print(value); }); print("main function end"); }Copy the code

The execution result

Main function start main function end hahahaCopy the code

Future.error(object) is similar to future.value (value)

2.1.3 Future.delayed(Time, callback function)

We can use Timer to realize the delayed task, or future. delayed can be used to realize the delayed task. Future.delayed can better handle the abnormal situation, and the encapsulation is optimized on the basis of Timer.

main(List<String> args) { print("main function start"); Future.delayed(Duration(seconds: 3), () {return "info 3 seconds later "; }).then((value) { print(value); }); print("main function end"); }Copy the code
2.1.4 Future comprehensive exercise
  • If the Future is not executed, then is added directly to the function body of the Future.
Future(() => print("future_1")). Then ((_) => print(" THEN_1 "));Copy the code
  • If the Future executes then, the body of the then function is put into a microtask queue, and the microtask queue is executed after the current Future executes
Future(() => null). Then ((_) => print("then_2"));Copy the code
  • If the Future is called, it means that the then is not executed, and the next THEN will not be executed
/ / future_3, then_3_a, then_3_b, in turn, join the eventqueue Future (() = > print (" future_3 ")), then ((_) = > print (" then_3_a ")), then ((_) = >  print("then_3_b"));Copy the code
import "dart:async";

main(List<String> args) {
  print("main start");

  Future(() => print("task1"));
  final future = Future(() => null);

  Future(() => print("task2")).then((_) {
    scheduleMicrotask(() => print('task4'));
  }).then((_) => print("task5"));

  future.then((_) => print("task6"));
  scheduleMicrotask(() => print('task7'));

  Future(() => print('task8'))
    .then((_) => Future(() => print('task9')))
    .then((_) => print('task10'));

  Future(() => print("task11"));
  print("main end");

Copy the code

The execution result

main start
main end
Copy the code

What if future F1’s operation returns another Future F2? If the result itself is a Future, f1 will not be completed. We might be forced to detect if the incoming argument is a future when onValue is called, deal with it through another then(), and so on, in an infinite loop. Fortunately, in this case, the completion value of F1 depends on the completion value of F2. When f2’s completion value is not future, F1 will complete with the same value. So we say futuresare chained because they form a chain of dependencies.

Ex 2

void testFuture() {
  Future f1 = new Future(() => print('f1'));
  Future f2 = new Future(() =>  null);
  Future f3 = new Future.delayed(Duration(seconds: 1) ,() => print('f2'));
  Future f4 = new Future(() => null);
  Future f5 = new Future(() => null);

  f5.then((_) => print('f3'));
  f4.then((_) {
    new Future(() => print('f5'));
    f2.then((_) {
  f2.then((m) {

Copy the code

The output

com.example.flutter_dart_app I/flutter: f8
com.example.flutter_dart_app I/flutter: f1
com.example.flutter_dart_app I/flutter: f7
com.example.flutter_dart_app I/flutter: f4
com.example.flutter_dart_app I/flutter: f6
com.example.flutter_dart_app I/flutter: f3
com.example.flutter_dart_app I/flutter: f5
com.example.flutter_dart_app I/flutter: f2
Copy the code

2.2 await, async

As we’ve seen, interacting directly with Future can be awkward. Part of the reason is that the classical control structures we’re familiar with don’t allow for asynchrony. Once an asynchronous call is made, it becomes the programmer’s responsibility to do all the necessary work to track whether the call was made, whether it was successful, and what the result was. Scheduling future work, scheduling follow-up work when it’s done, determining success or failure, and so on, can be daunting. To ease the pain of using asynchronous operations, Dart provides language-level support for asynchronous functions. The body of a function can be labeled with the async modifier, and the labeled function is an async function.

Future<int> foo() async => 42

Using async functions simplifies the task of handling futures and asynchrony in several ways. When an async function is called, the code of the function is not executed immediately. Instead, the code in the function is scheduled to execute at some future time. What does the function return to the caller? The future that is completed when the body of a function succeeds or fails. The future is automatically generated by the function and immediately returned to the caller.

Await expressions allow us to write asynchronous code as if we were writing synchronous code. Executing await expressions allows us to pause running surrounding functions while we wait for asynchronous computation to complete. Await can only be used in asynchronous functions. The Dart compiler does not accept waiting elsewhere.

They allow us to use synchronous code formats to implement asynchronous invocation procedures.

Future<String> getNetworkData() async { var result = await Future.delayed(Duration(seconds: 3), () { return "network data"; }); Return "request data:" + result; }Copy the code
2.2.1 coroutines

If you want to understand the principle of async and await, you should first understand the concept of coroutines. Async and await are essentially a syntactic sugar of coroutines. A coroutine, also known as a coroutine, is a unit smaller than a thread. In terms of unit size, it can basically be understood as process -> thread -> coroutine.

Coroutines are divided into wireless coroutines and wired coroutines,

  • The wireless coroutine places the current variable in the heap when it leaves the current calling location, and continues to fetch variables from the heap when it returns to the current location. Therefore, variables are allocated directly to the heap when the current function is executed, and async and await are one of the wireless coroutines.
  • Wired coroutines keep variables on the stack and continue to take calls off the stack when they return to the point where the pointer points.

2.3 isolate

The ISOLATE is a computing process with its own memory and single thread control. Being an ISOLATE results from the separation of independent entities, because the memory between isolates is logically separate. The code in the ISOLATE runs sequentially, and any concurrency is the result of running multiple isolates. Because DART has no shared memory concurrency, no locks are required and no contention is possible. Since the ISOLATE has no shared memory, the only way they can communicate with each other is through messaging. Messaging in DART is always asynchronous.

An ISOLATE has multiple ports. Port is the underlying implementation of dart ISOLATE communication. There are two types of ports: Send Port and Receive Port. Receive port is a stream that receives messages; The Send port allows messages to be sent to the ISOLATE or, more specifically, to the Receive port. A send port can be generated by a Receive port, which sends all messages to the corresponding Receive port.

We already know that Dart is single-threaded, and that this thread has its own memory space that it can access and an event loop that it needs to run; We can call this spatial system an Isolate; For example, there is a Root Isolate in Flutter, which is responsible for the code that runs Flutter, such as UI rendering, user interaction, and so on. Within the Isolate, resource isolation is very good. Each Isolate has its own Event Loop and Queue.

The Isolate does not share any resources and communicates with each other only through the message mechanism. Therefore, resource preemption is not a problem. However, having only one Isolate meant that we could only use one thread forever, which was a waste of resources for multi-core cpus.

If we had a lot of time consuming computations during development, we could create our own Isolate and do the computations within the Isolate.

LoadData () async {// Create a ISOLATE using spawn and bind the static method ReceivePort ReceivePort =ReceivePort(); await Isolate.spawn(dataLoader, receivePort.sendPort); SendPort SendPort = await receivePort.first; // Get new ISOLATE listener port SendPort = await receivePort. / / call sendReceive custom method List dataList = await sendReceive (sendPort, ''); print('dataList $dataList'); Static dataLoader(SendPort SendPort) async{// Create a listener port, ReceivePort ReceivePort =ReceivePort(); sendPort.send(receivePort.sendPort); Var MSG in receivePort) {String requestURL = MSG [0]; SendPort callbackPort =msg[1]; // The callback returns the value to the caller callbackport.send (' message '); }} // Create your own listener port, Send a message to the new ISOLATE Future sendReceive(SendPort SendPort, String URL) {ReceivePort ReceivePort =ReceivePort(); sendPort.send([url, receivePort.sendPort]); // Return receiveport.first; }Copy the code

Starting another ISOLATE in an ISOLATE is called the spawning. To make an ISOLATE, you need to specify a library. The isolate executes the library from the main() method of the ISOLATE. This library is called the root library of the ISOLATE. The Isolate class provides two class methods for generating isoli: the first is spawnUri(), which generates an Isolate based on the URI of a given library; The second is spawn(), which generates an ISOLATE based on the root repository of the current ISOLATE


As a cross-platform UI framework, Flutter has its own rendering mechanism. However, Flutter App ultimately needs to run on the Native platform. The code execution, task execution, page rendering and other operations ultimately depend on the CPU of the Native platform and still need the thread management mechanism of the operating system. So how do the isolators and Native Threads of Flutter collaborate ??????????

Native Processes and Threads (iOS)

Each App running on the operating system is regarded as a process, with independent memory space that is not shared with other processes. The process communicates through port. After the App is started, the main thread is opened by default. Responsible for handling tasks (source0, source1, etc.)

  • Source0 is the task from within this thread
  • Source1 is the task sent through port

UI rendering and other operations must be performed in the main thread. Time-consuming tasks (network requests) must be executed by child threads. After receiving the results, the main thread can be called back

Iv. Flutter thread management and Dart Isolate mechanism

The Flutter Engine requires the Embeder to provide four Task Runners, which refers to the middle-tier code that ports the Engine to the platform. The four main Task runners include:

4.1 Platform Task Runner

The Main Task Runner of the Flutter Engine is similar to the Android Main Thread or iOS Main Thread. But it’s important to note that there are differences.

Generally, when a Flutter application is started, an Engine instance is created. When the Engine is created, a thread is created for Platform Runner to use.

All interactions with the Flutter Engine (interface calls) must occur on the Platform Thread. Otherwise, unexpected exceptions may result. This is similar to how iOS UI operations must be performed on the main thread. Note that there are many modules in the Flutter Engine that are not thread-safe.

The rule is simple: All calls to the Flutter Engine interface must be made on the Platform Thread.

Blocking the Platform Thread does not directly cause the Flutter application to stall (unlike the iOS android main Thread). However, it is not recommended to perform heavy operations on the Runner. If the Platform Thread is stuck for a long time, it may be forcibly killed by the system Watchdog.

4.2 UI Task Runner Thread (Dart Runner)

The UI Task Runner is used to execute the Dart root ISOLATE code (the ISOLATE, which we’ll cover later, is simply the thread inside the Dart VM). The Root isolate is special in that it binds many of the functions that Flutter needs to perform rendering operations. For each frame, the engine does the following:

Root ISOLATE notifies the Flutter Engine that a frame needs to be rendered. The Flutter Engine notifies the platform that it needs to be notified at the next vsync. The platform waits for the next vSync to Layout the created objects and Widgets and generate a Layer Tree. This Tree is immediately submitted to the Flutter Engine. No rasterization is done in the current phase; this step simply generates a description of what needs to be drawn. Create or update a Tree that contains semantic information for displaying Widgets on the screen. This thing is mainly used to configure and render platform-specific auxiliary Accessibility elements. In addition to rendering logic, Root Isolate also handles messages from Native Plugins, Timers, Microtasks, and asynchronous IO. The Root Isolate is responsible for creating and managing the Layer Tree and ultimately deciding what to draw on the screen. Therefore, the overload of this thread will directly cause frame lag.

4.3 the GPU Task Runner

GPU Task Runner Is used to execute GPU commands. The Layer Tree created by UI Task Runner is cross-platform and doesn’t care who does the drawing. GPU Task Runner is responsible for translating the information provided by Layer Tree into platform-executable GPU instructions. GPU Task Runner is also responsible for the management of GPU resources required for drawing. Resources mainly include platform Framebuffer, Surface, Texture and Buffers, etc.

Generally speaking UI Runner and GPU Runner run on different threads. GPU Runner will ask UI Runner for the data of the next frame according to the progress of the current frame execution, and may tell UI Runner to delay the task when the task is heavy. This scheduling mechanism ensures that THE GPU Runner is not overloaded, while avoiding unnecessary consumption of UI Runner.

It is recommended to create a dedicated GPU Runner thread for each Engine instance.

4.4 I/o Task Runner

Several runners discussed earlier have high requirements for execution fluency. Overload of Platform Runner may cause system WatchDog to forcibly kill, while overload of UI and GPU Runner may cause lag of Flutter applications. But where do some of the necessary operations of the GPU thread, such as IO, go? The answer is IO Runner.

IO Runner’s main function is to read compressed image formats from image storage (such as disk) and process the image data in preparation for GPU Runner’s rendering. IO Runner starts by reading compressed image binary data (such as PNG and JPEG), decompressing it into a format that the GPU can process, and uploading the data to the GPU.

Obtaining resources such as UI. Image can only be called via an async call. When the call occurs, the Flutter Framework tells IO Runner to perform an asynchronous operation to load.

IO Runner directly determines that the loading delay of images and other resources indirectly affects performance. It is recommended to create a dedicated thread for IO Runner.

Each Engine instance on the Mobile platform starts with a new thread for the UI, GPU, and IO Runner. All Engine instances share the same Platform Runner and thread.

4.5 Flutter Engine Runners and Dart Isolate

Runner is an abstract concept. We can submit tasks to Runner, and the tasks will be executed by Runner in its thread, which is similar to the execution queue of iOS GCD. In fact, there is a loop in the implementation of iOS Runner. This loop is CFRunloop, and the specific implementation of Runner on iOS platform is CFRunloop. The submitted task is placed in the CFRunloop for execution.

The Dart Isolate is managed by the Dart VM and cannot be directly accessed by the Flutter Engine. The Root Isolate uses Dart’s C++ call capability to submit UI rendering tasks to UI Runner so that they can interact with Flutter Engine modules. The tasks associated with Flutter UI are also submitted to UI Runner to notify the Isolate of events. UI Runner also handles tasks from the App Native Plugin.

Therefore, Dart ISOLATE and Flutter Runner are independent of each other. They cooperate with each other through task scheduling mechanism.

Refer to the article

Asynchronous programming: Use Future and async-await

Dart Asynchronous task and message loop mechanism

Asynchronous: Dart-Future and Microtask execution order

Discuss Flutter thread management and Dart Isolate

Thoroughly understand Dart asynchrony

In-depth understanding of Flutter multithreading

Deep understanding of Flutter multithreaded | · DTalk developers said