The Event Loop mechanism
Just like iOS Runloop, Flutter has an event loop, but Dart is single-threaded. So what does a single thread mean? This means that the Dart code is ordered, executing one after the other in the order in which it appears in the main function, without being interrupted by other code. Dart also supports asynchronism, which is the key technology supporting the UI framework of Flutter. Note that single-threaded and asynchronous are not in conflict.
Why can a single thread be asynchronous?
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.
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 wait behavior is driven by an Event Loop. The Event Queue places events from other parallel worlds (such as sockets) that need to be responded to by the main thread. Dart, like other languages, has a huge event loop, polling the event queue, fetching events (for example, keyboard events, I\O events, network events, etc.), and executing its callback function in the main thread in step, as shown in the figure below:
Asynchronous tasks
The Event Loop diagram in Figure 1 is just a simplified version. In Dart, there are actually two queues, an Event Queue and a Microtask Queue. During each event loop, Dart checks the first microtask queue to see if there are any available tasks, and if there are, it processes all of them. If not, the subsequent process of the event queue is processed.
The full version of the Event Loop flowchart should look like this:
First, let’s look at the microtask queue. A microtask, as its name suggests, is an asynchronous task that is completed in a short period of time. As you can see from the above flow chart, the microtask queue has the highest priority in the event cycle and can occupy the event cycle as long as there are tasks in the queue.
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
Asynchronous tasks rarely have to be completed in front of an event queue and therefore do not require high priority. Therefore, microtask queues are rarely used directly, even within Flutter (for example, Gesture recognition, text entry, scrolling view, saving page effects, etc.).
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.
Dart provides a layer of encapsulation for the task creation of the Event Queue called the Future. ** is also easy to understand from its name; it represents a task that will be completed in the future.
By putting a function body into a Future, you complete the packaging from synchronous to asynchronous tasks. 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.
Next, let’s look at a concrete code example that declares two asynchronous tasks separately and outputs a string in the next event loop. After the second task completes, two more strings are printed:
Future(() => print('Running in Future 1')); Future(() => print(' Running in Future 2')). Then ((_) => print('and then 1')). Then ((_) => print('and then 1') 2 ')); // Output three consecutive strings after the last event loopCopy the code
Of course, the execution priority of the two Asynchronous Future tasks is lower than that of the microtasks.
Normally, the execution of a Future asynchronous task is relatively simple: When we declare a Future, Dart puts the function body of the asynchronous task into an 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 ** THEN shares an event loop with the Future body. ** If a Future has more than one THEN, they are executed synchronously in the order in which they were called, and 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.
The following code demonstrates the Future’s execution rules, that is, the event is queued first, or the task declared first is executed first; Then executes immediately after the Future ends.
Future(() => print('f1')); Future(() => print('f2')); / / f3 execution will immediately after the synchronous execution then 3 Future (() = > print (' f3 ')), then ((_) = > print (' then 3)); / / then 4 will join task queue, as soon as possible to perform Future (() = > null). Then ((_) = > print (' then 4 '));Copy the code
- In the first example, since F1 is declared before F2, it is added to the event queue first, so f1 executes before F2;
- In the second example, since the Future body shares an event loop with THEN, f3 immediately synchronizes then 3;
- In the last example, the body of the Future function is null, which means that it does not need or have an event loop, so subsequent THEN cannot be shared with it. In this scenario, Dart puts the subsequent THEN into the microtask queue and executes it in the next event loop.
Through a comprehensive case, to the previous introduction of the various execution rules are linked together, and then focus on learning. In the example below, we declare several asynchronous task Futures, as well as microtasks. In some of these futures, we have embedded Future and MicroTask declarations:
Future(() => print('f1')); // Declare an anonymous Future Future fx = Future(() => null); // Declare an anonymous Future and register two THEN's. In the first then callback started a micro task Future (() = > print (' f2 ')). Then ((_) {print (' f3 '); scheduleMicrotask(() => print('f4')); }).then((_) => print('f5')); // Declare an anonymous Future and register two THEN. First then is a Future Future (() = > print (' f6)), then ((_) = > Future (() = > print (' f7))), then ((_) = > print (" f8 ")); Future(() => print('f9')); Then fx.then((_) => print('f10')); ScheduleMicrotask (() => print('f11')); print('f12');Copy the code
Don’t rush down to see the results, write in a small notebook, or run their own code
Run it and each of the above asynchronous tasks will print its internal execution results in turn:
f12
f11
f1
f10
f2
f3
f5
f4
f6
f9
f7
f8
Copy the code
By this point, you’re probably confused. Let’s take a look at the changes in the Event Queue and Microtask Queue during the execution of this code, and see why they are executed in this order:
- Since all other statements are asynchronous tasks, print F12 first.
- Of the remaining asynchronous tasks, the microtask queue has the highest priority, so f11 is printed later; Then print F1 in the order in which the Future declaration was made.
- Dart puts fx’s THEN into the microtask queue because the microtask queue has the highest priority, so fx’s then is executed first and f10 is printed.
- Then go to F2 below FX, print F2, and then print F3. F4 is a microtask that is not executed until the next event loop, so subsequent THEN continues to be executed synchronously, printing F5. When this event loop ends, the next event loop takes out the f4 microtask and prints F4.
- Then go to F6 below F2, print F6, and execute then. The important thing to note here is that this THEN is a Future asynchronous task, so this THEN and subsequent THEN are put into the event queue. Subsequent THEN’s are also placed in the event queue because the arrow function returns a future, so subsequent THEN’s follow it. If there is no return future, then’s follow the previous synchronization
- F6 and f9. Print F9.
- For the last event loop, print F7, followed by F8.
The only thing you need to remember is that then is executed immediately after the body of the Future function completes execution, whether sharing the same event loop or going to the next microtask.
With a deeper understanding of the execution rules for Future asynchronous tasks, let’s look at how to encapsulate an asynchronous function.
An asynchronous function
For an asynchronous function, its internal execution does not end when it returns, so it needs to return a Future object for the caller to use. According to the Future object, the caller decides whether to register a THEN on the Future object and wait for the Future to be processed asynchronously after the implementation body finishes. Or wait synchronously for the Future implementation to end.
For a Future object returned by an asynchronous function, if the caller decides to wait synchronously, the await keyword is used at the invocation and the async keyword is used in the function body at the invocation.
In the following example, the asynchronous method returns a Hello 2019 with a 3-second delay, and we wait with await at the call until it returns:
// Declare a Future that returns Hello with a 3-second delay, FetchContent () => Future<String>.delayed(Duration(seconds:3), () => "Hello") .then((x) => "$x 2019"); main() async{ String str = await fetchContent() print(str); // Wait for Hello 2019 to return}Copy the code
You may have noticed that we have added the async keyword to the call context of the waiting statement main while waiting with await. Why add this keyword?
Because 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.
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 => await Future(() => 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.
Next, let’s look at another example where the main function calls an asynchronous function to print a paragraph, and in this asynchronous function we await with async another asynchronous function to return a string:
// Declare a Future that returns Hello after 2 seconds, FetchContent () => Future<String>.delayed(Duration(seconds:2), () => "Hello") .then((x) => "$x 2019"); Async => print(await fetchContent()); async => print(await fetchContent()); main() { print("func before"); func(); print("func after"); }Copy the code
Running this code, we see that the final output order is actually “func before” “func after” “Hello 2019”. The wait statement in the func function doesn’t seem to work. Why is that?
Let me show you the order in which this code is executed:
- First, the first line of code is synchronized, so print “func before” first.
- Then we enter the func function, which calls the asynchronous function fetchContent and waits with await, so we queue the fetchContent and await context function func.
- The await context function does not contain a call stack, so the subsequent func code continues to execute, printing “func after”.
- After 2 seconds, the fetchContent asynchronous task returns “Hello 2019”, and the await of the func is also taken out, printing “Hello 2019”.
What did you find out from the above analysis? Await and async are only valid for functions in the calling context and are not passed up. So for this case, funC is waiting asynchronously. If we want to wait synchronously in main, we need to call async as well as await the asynchronous function.
Everybody big guy should still be confused, see a few times more, knock a few times more nature will understand
conclusion
In UI programming, asynchrony and multithreading are two concomitant terms, and also very confusing concepts. For asynchronous method invocations, the code does not wait for the result to return, but rather actively (or passively) receives the result of execution at a later point through other means (such as notifications, callbacks, event loops, or multithreading).
Therefore, from the dialectical relationship, asynchrony and multithreading are not an equal relationship: asynchrony is the goal, multithreading is only one of the means we realize asynchrony. With Flutter, thanks to the event loop provided by the UI framework, we can wait for multiple asynchronous tasks without blocking, so there is no need for multithreading. We must keep that in mind.
Q/A
- Suppose there is a task (reading and writing a file or network) that takes 10 seconds and is added to the event task queue, wouldn’t the thread be blocked while performing that task alone?
File I/O and network calls are not made at the Dart layer, but rather by asynchronous threads provided by the operating system. The Dart code performs a simple read as soon as the work is done and the results are queued.
- The single thread model refers to the event queue model, and the drawing interface thread is the same
By single threading we mean the main Isolate. On the other hand, GPU drawing instructions are executed by a separate thread, independent of the main Isolate. Platform Task Runner: processes messages from the Platform (Android/iOS).UI Task Runner: processes messages from the Platform (Android/iOS). Execute rendering logic, process native plugin messages, timer, microtask, and asynchronous I/O operation processing, etc. 3.GPU Task Runner: execute GPU instruction 4. In addition, the operating system also provides a large number of asynchronous concurrency mechanisms. You can use multiple threads to execute tasks (such as sockets), but we don’t need to worry about them in main Isolate (if you really want to create concurrent tasks, you can do so).
/ / the first period of the Future (() = > print (' f6)), then ((_) = > Future (() = > print (' f7))), then ((_) = > print (" f8 ")); The execution result is: the f6 f7 f8 / / the second period of the Future (() = > print (' f6). Then ((_) {Future (() = > print (' f7 ')); }) .then((_) => print('f8')); F6, f8, f7Copy the code
The one-line arrow function is a Future, and that’s not the same thing as having a Future in the body of the function. Then returns the Future, so if you use =>, the new Future will be returned by then, and the caller to the second THEN is the new Future object, So the second THEN doesn’t follow the old future but its new future. If I put the => in curly braces, then the second “then” is still the same as the old future, it doesn’t matter what the new future is
-
When is the next singularity
In 2045,