Dart is a single-threaded execution, so how does it work asynchronously?
This paper will analyze the knowledge points related to asynchronous operations such as THE Isolate,Event Loop,Future,async/await provided by Dart/Flutter.
Isolate
What is Isolate?
An isolate is what all Dart code runs in. It’s like a little space on the machine with its own, private chunk of memory and a single thread running an event loop.
- IsolateThe equivalent of
Dart
Threads in a languageThread
, it isDart/Flutter
Execution context (container); - IsolateHas its own independent memory address and
Event Loop
, there is no shared memory so there will be no deadlock, but thanThread
More memory consumption;
- IsolateThere is no direct access, need to rely on
Port
Communicate;
Main Isolate
After executing the main() entry function, the Flutter creates a Main Isolate. Generally, tasks are performed within the Main Isolate.
multithreading
Generally, tasks can be performed on the Main Isolate. However, if some time-consuming operations are performed on the Main Isolate, frames are deleted, which adversely affects user experience. In this case, distribute time-consuming tasks to other ISOLates.
All Dart codes are executed in the Isolate. The Dart codes can only use the contents of the same Isolate. Different ISOLATES are isolated in memory.
case
We did a simple Demo with an animated heart in the middle of the screen (from small to large, then from large to small). When we click the right plus button in the lower right corner, a time-consuming calculation is performed. If a time-consuming operation is performed in the Main Isolate, frames are lost on the interface and the animation is stuck.
That’s what we need to fix right now.
1.compute
methods
The compute API encapsulates a high-level function that allows us to easily implement multi-threaded functionality.
Future<R> compute<Q, R>(isolates.ComputeCallback<Q, R> callback, Q message, { String? debugLabel }) async {
}
Copy the code
Compute takes two mandatory parameters: 1, the method to execute; 2. The parameters passed in can only be 1 at most, so multiple parameters need to be encapsulated in the Map;
- The first code
'bigCompute' Future<int> bigCompute(int initalNumber) async {int total = initalNumber; for (var i = 0; i < 1000000000; i++) { total += i; } return total; } // Click the button to call the method: 'calculator' void calculator() async {int result = await bigCompute(0); print(result); } // Click on FloatingActionButton(onPressed: Calculator, Tooltip: 'Increment', Child: Icon(Icons.add), )Copy the code
- Modify the code
- Create a new
calculatorByComputeFunction
Method of usecompute
callbigCompute
Methods:
Void calculatorByComputeFunction () is async {/ / using ` compute ` call ` bigCompute ` method, Int result = await compute(bigCompute, 0); print(result); }Copy the code
- Modify theFloatingActionButtonThe click event method of is
calculatorByComputeFunction
FloatingActionButton(
onPressed: calculatorByComputeFunction,
tooltip: 'Increment',
child: Icon(Icons.add),
)
Copy the code
Let’s click on it. Okay?
[VERBOSE-2:ui_dart_state.cc(186)] Unhandled Exception: Invalid argument(s): Illegal argument in isolate message : (object is a closure – Function ‘bigCompute’:.)
- To solveErrorWill:
bigCompute
Instead ofstaticMethod (changing to a global function is also possible)
static Future<int> bigCompute(int initalNumber) async {
int total = initalNumber;
for (var i = 0; i < 1000000000; i++) {
total += i;
}
return total;
}
Copy the code
Warning: All platform-channel communication must operate within the Main Isolate. For example, calling Rootbundle. loadString(“assets/***”) in another Isolate may fail.
2. Use directlyIsolate
We did not see the Isolate, because the Flutter helps us create and destroy the Isolate, execute the method, etc. This is usually enough for us to use.
However, the drawback of this method is that we can only perform one task. When we have several similar time-consuming operations, using this compute method will cause a lot of creation and destruction, which is a high cost process. If we can reuse THE Isolate, it is the best way to implement it.
The principle of multi-threaded inter-ISOLATE communication is as follows:
-
The logic for the current Isolate to receive messages from other ISOLATES is as follows: The ReceivePort is the receiver. It is equipped with a SendPort sender. The current Isolate can send SendPort sender to other ISOLATES. The SendPort sender allows other ISOLATES to send messages to the current Isolate.
-
The implementation logic of the current Isolate sending messages to other ISOLATES is as follows: Other ISOLATES send SendPort2 sender 2 through SendPort sender of the current Isolate. Other ISOLATES hold ReceivePort2 receiver 2 corresponding to SendPort2 sender 2. If the CURRENT Isolate sends messages through SendPort 2, other ISOLATES can receive the messages.
Isn’t that convoluted! Here’s another metaphor: there’s a communications suite. The communications suite consists of a tool for receiving calls and a tool for making calls. A saves the phone taker and sends the caller to B, so that B can call A anytime and anywhere (this is one-way communication). If B also has A set of tools and sends the caller to A, then A can also call B anytime and anywhere (two-way communication now).
The code:
Class _MyHomePageState extends the State < MyHomePage > with SingleTickerProviderStateMixin {/ / 1.1 new isolate isolate isolate; // 1.2 Main Isolate receiver ReceivePort mainIsolaiteReceivePort; // 1.3 SendPort otherIsolateSendPort of Other Isolate; Void spawnNewIsolate() async {// 2.1 Create a receiver to receive the Main Isolate if (mainIsolaiteReceivePort == null) { mainIsolaiteReceivePort = ReceivePort(); } try {if (isolate == null) {// 2.2 Create an ISOLATE and transfer the Main ISOLATE to the new one. CalculatorByIsolate is required to perform the task of isolate = await isolate the spawn (calculatorByIsolate, mainIsolaiteReceivePort. SendPort); / / 2.3 Main Isolate through the messages from the receiver to receive the new Isolate mainIsolaiteReceivePort. Listen ((dynamic message) {if (message is SendPort) { // 2.4 If the new ISOLATE sends a message to the new ISOLATE, send the message to the new ISOLATE through the message sender. OtherIsolateSendPort = Message. otherIsolateSendPort.send(1); Print (" Two-way communication is established successfully, the main ISOLATE transmits the initial parameter 1"); } else {// 2.5 If the new ISOLATE sends a value, we know it is the result of a time-consuming operation. Print (" $message = new ISOLATE "); }}); } else {otherIsolateSendPort! = null) { otherIsolateSendPort.send(1); Print (" bidirectional communication multiplexing, main ISOLATE transmits initial parameter 1"); }}} catch (e) {}} // This is the task executed in the new Isolate static void calculatorByIsolate(SendPort SendPort) {// 3.1 The new Isolate sends the sender to Main Isolate ReceivePort ReceivePort = new ReceivePort(); sendPort.send(receivePort.sendPort); Receiveport. listen((val) {print(" The initial parameter passed from the Main Isolate is $val"); // 3. int total = val; for (var i = 0; i < 1000000000; i++) { total += i; } // 3.3 Send the result sendport. send(total) through the sender of Main Isolate. }); } @ override void the dispose () {/ / release resources mainIsolaiteReceivePort. Close (); isolate.kill(); super.dispose(); }}Copy the code
The code is commented in detail, so I won’t explain it anymore. Is not a lot of code feeling, in fact, if you understand the process of logic is not complex.
This is the introduction of the concept and usage of THE Isolate. Next, we will introduce an important knowledge point of the Isolate, Event Loop.
Event Loop
Loop is a concept that most developers should be familiar with. There is NSRunLoop in iOS, Looper in Android, and Event Loop in JS. They have similar names, but they do similar things.
The official introduction of Event Loop is as follows:
- Static diagram
After executing the main() function, a main Isolate is created.
- Dynamic diagram
- Event LoopTwo queues are processed
MicroTask queue
andEvent queue
Tasks in; Event queue
Mainly handles external event tasks:I/O
.Gesture events
.The timer
.Communication between isolates
And so on;MicroTask queue
Mainly deal with internal tasks: such as processingI/O
Some special handling that may be involved in the intermediate process of the event;- Both queues have first-in, first-out processing logic and are processed first
MicroTask queue
The task whenMicroTask queue
Execute the command only after the queue is emptyEvent queue
Tasks in; - GC is performed when both queues are empty, or simply waiting for the next task to arrive.
In order to better understand the asynchronous logic of Event Loop, let’s make an analogy: it is like the process when I went to buy a cup of “Orchid latte” (it is time-consuming because it is freshly made tea) at a famous Internet milk tea brand store in Changsha.
- I went to the front desk and told the clerk THAT I would like to buy one of your “Orchid lattes,” and the clerk handed me a numbered frisbee.
- The food preparation staff of the milk tea shop put my order at the bottom of the order list. They prepared the items on the order in order. When one item was ready, the customer was sent to pick it up (the Event queue is first in, first out and processed), while I walked away and went on with my work (asynchronous process, no waiting for the result).
- All of a sudden they get an order from a super VIP member, and the caterer puts the super VIP order first and prioritizes the items in the order (MicroTask first)– this is a fictional scenario;
- When my order is completed, frisbee began shaking (the callback), I once again back to the front desk, if sister handed me a cup of milk tea reception (results), if the front desk sister said I’m sorry, Sir, in the time of your order no water, the orders can not finished give me my money back (exception error error).
Our common asynchronous operations Future,async, and await are all based on Event loops. We will introduce the principles behind their asynchronous operations.
Future
Let’s take a look at the logic behind a Future in general:
final myFuture = http.get('https://my.image.url'); myFuture.then((resp) { setImage(resp); }).catchError((err) { print('Caught $err'); // Handle the error. }); // Continue with other tasks...Copy the code
http.get('https://my.image.url')
An unfinished state is returnedFuture
Can be understood as a handle, andhttp.get('https://my.image.url')
Was thrown into theEvent queue
Wait to be executed, and then proceed to execute the other current task;- when
Event queue
So after we execute thisget
A callback is performed when the request succeedsthen
Method to return the result,Future
To complete, proceed with the next operation;- when
Event queue
So after we execute thisget
The request is called back if it failscatchError
Method to return the error,Future
Is in the failed state, error handling is available.
Let’s take a look at some functions related to Future:
The constructor
Future(FutureOr<T> computation())
final future1 = Future(() {
return 1;
});
Copy the code
Computation is placed in the Event queue
Future.value
final future2 = Future.value(2);
Copy the code
The value is returned in the MicroTask Queue
Future.error(Object error, [StackTrace? stackTrace])
final future3 = Future.error(3);
Copy the code
This error indicates that an error occurred, and the value does not necessarily need to be given an error object
Future.delay
final future4 = Future.delayed(Duration(seconds: 1), () {
return 4;
});
Copy the code
Delay execution for a certain period of time
Future result callbackthen
Final Future = future.delayed (Duration(seconds: 1), () {print(' calculate '); return 4; }); future.then((value) => print(value)); Print (' proceed to the next task '); // flutter: Move on to the next task // flutter: calculate // flutter: 4Copy the code
A callback for a Future erroronError
final future = Future.error(3); future.then((value) => print(value)) .onError((error, stackTrace) => print(error)); Print (' proceed to the next task '); // Flutter: Move on to the next task // flutter: 3Copy the code
The callback completed by the FuturewhenComplete
final future = Future.error(3); Future.then ((value) => print(value)).onError((error, stackTrace) => print(error)).whencomplete (() => print(" finish ")); Print (' proceed to the next task '); // flutter: Move on to the next task // flutter: 3 // flutter: doneCopy the code
async/await
Those of you who have done front-end development should be familiar with these two keywords, but async/await in Flutter is essentially just syntax sugar for Future and is easy to use.
Future<String> createOrderMessage() async {
var order = await fetchUserOrder();
return 'Your order is: $order';
}
Copy the code
await
Put the return value asFuture
In front of the execution task, it is equivalent to making a mark, indicating that the execution ends here, and then go down to execute after there is a result;- Using the
await
Must be added after the methodasync
;async
Methods must be encapsulated on the return valueFuture
.
FutureBuilder
Flutter encapsulates a FutureBuilder Widget that can be used to easily build uIs, for example, to get images for display:
FutureBuilder(Future: Future to load images uilder: (context, snapshot) { Snapshot.hasdata) {// Use the default placeholder image} else if (snapshot.haserror) {// Use the failed image} else {// Use the loaded image}},Copy the code
conclusion
You can create an Isolate to implement multiple threads. Each thread Isolate has an Event Loop to perform asynchronous operations.
Some mobile developers may prefer ReactiveX responsive programming, such as RxJava, RxSwift, ReactiveCocoa, etc. They are also a form of asynchronous programming, and Flutter provides a corresponding class, Stream, which also has rich intermediate operators and provides a StreamBuilder to build UIs, which we will examine in the next article.