preface
We know that the Flutter framework has excellent rendering and interaction capabilities. Behind these complex capabilities is Dart, which is based on a single-threaded model. How does the single-threaded Dart ensure the smoothness of the Flutter UI in terms of language design and code execution compared to the native Android and iOS multithreading mechanisms?
Let’s elaborate from the following aspects:
- Dart language single-threaded model and Event Loop processing mechanism
- Principles and usage of asynchronous processing and concurrent programming
- The nature of code execution under the SINGLE-threaded Dart model
1. Dart single-threaded model
Dart is run single-threaded. How to understand this sentence, from the following aspects can see this design idea.
1.1 Default single running thread
Dart runs in Main by default and has threads called ISOLATE in DART. This thread can be called Main ISOLATE. Single-thread tasks are processed on the main ISOLATE by default if the new ISOLATE is not enabled. Once the Dart function is executed, it continues to be executed in the order in which the main function appears, one after the other, until it exits. In other words, the Dart function cannot be interrupted by other Dart code during execution.
1.2 Exclusive Memory
Android and IOS are free to create threads other than the UI main thread, which can share memory variables with the main thread. However, the ISOLATE in Dart cannot share memory. The Isolate does not share memory. They are like separate apps that communicate via messages. Except for explicitly specifying that the code runs on another ISOLATE or worker, all other code runs on the main ISOLATE of the app. For more information, visit Use or workers if necessary
1.3 question
(1) Suppose there is a task (reading and writing files or network) that takes 10 seconds and is added to the event task queue, wouldn’t the thread block the master when executing this task alone?
A: File I/O and network calls are not done at the Dart layer, but rather by asynchronous threads provided by the operating system. The Dart code performs a simple read as soon as they finish their work and queue the results.
(2) The single thread model refers to the event queue model, and the drawing interface thread is the same?
A: What we mean by single thread is the main Isolate. On the other hand, GPU drawing instructions are executed by a separate thread, independent of the main Isolate. There are actually four types of Task runners that Flutter provides, with separate threads to run specific tasks: see also: Insight into the Flutter engine threading pattern
- Platform Task Runner: Handles messages from platforms (Android/iOS)
- UI Task Runner: performs rendering logic, handles native Plugin messages, timer, microtask, asynchronous I/O operation processing, etc
- GPU Task Runner: Executes GPU commands
- IO Task Runner: Performs I/O tasks
2. Event Loop mechanism
Dart also has event queues and event loops, as shown in the figure. Each ISOLATE also contains an event loop. The difference is that it has two event queues, the Event Loop, and the Event Queue and microTask Queue. Event and MicroTask queues are somewhat similar to iOS source0 and source1.
- 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 queues.
- First check
MicroTask
If the queue is empty, execute firstMicroTask
Microtasks in queues - a
MicroTask
After execution, check if there is a next oneMicroTask
Until theMicroTask
If the queue is empty, executeEvent
The queue - in
Evnet
After the queue takes out an event, it returns to the first step again to checkMicroTask
Whether the queue is empty
It can be seen that adding tasks to MicroTask can be executed as soon as possible. However, it should also be noted that when the event loop processes the MicroTask queue, the event execution of the event queue will be blocked, which will lead to the delay of event response such as rendering and gesture response. To ensure rendering and gesture response, place time-consuming operations on the Event queue as much as possible.
Microtask queues are rarely used directly, even inside Flutter, there are only seven applications (e.g. gesture recognition, text input, scrolling view, saving page effects, etc.).
It can be summarized as a one-two-one model: a single-threaded execution model with one event loop and two queues.
3. Asynchronous task scheduling
Why can a single thread be asynchronous? The big premise here is that our App spends most of its time waiting. For example, waiting for users to click, waiting for network requests to return, waiting for file IO results, and so on. And these wait behaviors are not blocking. For example, for network requests, the Socket itself provides a select model to query asynchronously; And file IO, the operating system also provides event-based callback mechanism. Therefore, based on these characteristics, the single-threaded model can do other things while waiting, and then do the corresponding processing when the response is actually needed. Because waiting isn’t blocked, it feels like we’re doing more than one thing at a time. But there’s only one thread handling your business.
The lower priority Event Queue is the one we use most for asynchronous tasks. For example, asynchronous events such as I/O, draw, and timers are executed through the event queue that drives the main thread.
3.1 Initiating asynchronous Tasks with the Future
Dart provides a layer of encapsulation for the task creation of the Event Queue, called the Future. The Future also provides the ability to make chain calls that execute other function bodies on the link in sequence after an asynchronous task has completed.
new Future((){
// doing something
});Copy the code
Microtasks are created by scheduleMicroTask. This code outputs a string in the next event loop, as shown below:
scheduleMicrotask(() => print('This is a microtask'));Copy the code
Chain call:
Future(() => print('Running in Future 1')); // Next event loop outputs the string Future(() =>print(' Runningin Future 2')) .then((_) => print('and then 1')) .then((_) => print('and then2 ')); // Output three consecutive strings after the last event loopCopy the code
Dart puts the body of the function execution of the asynchronous task into the event queue and immediately returns, with subsequent code continuing to execute synchronously. After the synchronized code is executed, the event queue will fetch the events in the order in which they were added to the event queue (i.e., the declaration order), and finally synchronously execute the function body of the Future and the subsequent THEN. This means that the then and Future function bodies share an event loop. If a Future has more than one THEN, they will be executed synchronously in the order of the chained calls, and will also share an event loop.
What does Dart do if you take a reference to a Future and add a then method body after the Future has already been executed? In this case, Dart puts subsequent THEN methods into the microtask queue and executes them as quickly as possible.
// Future(() =>print('f1'));
Future(() => print('f2')); //f3 will be synchronized immediately after executionthen 3
Future(() => print('f3')).then((_) => print('then 3'));
//thenFuture(() => NULL).then((_) =>print('then 4')); Results: F1, F2, F3then 3 then 4Copy the code
4. Asynchronous functions
Future is an encapsulation of asynchronous tasks. With await and async, we can achieve non-blocking synchronous waiting through event loops. Await in Dart is not blocking wait, it is asynchronous wait. Dart treats the calling body’s function as an asynchronous function, putting the context of the waiting statement into the Event Queue. Once the result is available, the Event Loop takes it out of the Event Queue and waits for the code to continue.
When the async keyword is used as a suffix for method declarations, it has the following meanings
- The decorated method will add a
Future
Object as the return value - The method synchronizes the code executing the method until the first await keyword, and then it suspends the rest of the method;
- As soon as the Future task referenced by the await keyword completes execution, the next line of await code will execute immediately.
// Import IO library, call sleep function import'dart:io'; // Simulate time-consuming operation, call sleep function sleep 2 secondsdoTask() async{
await sleep(const Duration(seconds:2));
return "Ok"; } // Define a function for wrappingtest() async {
var r = await doTask();
print(r);
}
void main() {print("main start");
test(a);print("main end"); } result: main start main end OkCopy the code
So let’s take a look at this code. The second then executor f2 is a Future. In order to wait for it to complete before proceeding with the next operation, we use await and expect to print the result f1, F2, F3, f4:
Future(()=>print('f1'))
.then((_)async=>awaitFuture(()=>print('f2')))
.then((_)=>print('f3'));
Future(()=>print('f4'));Copy the code
In fact, when you run this code, you will see that the printed results are actually F1, F4, F2, F3!
- Examine the order in which this code is executed:
- According to the task declaration order, F1 and F4 are added to the event queue successively.
- F1 is taken out and printed;
- And then we go to then. The executioner of then is a Future F2, so we put it in the Event Queue.
- Then put await in the Event Queue as well. Note that there is also an F4 in the Event Queue and that our await cannot block execution of F4. Therefore, the Event Loop first extracts F4 and prints F4.
- F2 can then be taken out and printed, and finally await is taken out, followed by F3.
Since await uses the event queue mechanism to wait, F4 that is in the event queue before it will not be blocked by it.
5. Isolate
Dart also provides a multi-threaded mechanism called Isolate (the Chinese word for isolation). Each Isolate has its own Event Loop and Queue. The Isolate does not share any resources and can only communicate with each other through the message mechanism. Therefore, resource preemption is not a problem. As shown below, we declared an entry function to Isolate, then started it in main and passed a string argument:
doSth(msg) => print(msg);
main() {
Isolate.spawn(doSth, "Hi"); . }Copy the code
So how to use the message mechanism for communication, the following quoted an article on the explanation, the picture is very good.
The entire message communication process is shown in the figure above. The two Isolate communicate with each other through two pairs of Ports, which are respectively ReceivePort for receiving messages and SendPort for sending messages. The SendPort object is not created separately; it is already included in the ReceivePort object. Note that a pair of Port objects can send messages only in one direction, just like a water pipe. ReceivePort and SendPort are located at two ends of the water pipe. Water flows from SendPort to ReceivePort. Therefore, message communication between two ISOLates must require two such water pipes, which requires two pairs of Port objects.
6. Cite the article
23 | single thread model (1) how to ensure the smooth UI operation?
(2) Complete explanation of Dart asynchronous programming
(3) Dart asynchronous programming: Computers and Event loops