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 ofDartThreads in a languageThread, it isDart/FlutterExecution context (container);
  • IsolateHas its own independent memory address andEvent Loop, there is no shared memory so there will be no deadlock, but thanThreadMore memory consumption;

  • IsolateThere is no direct access, need to rely onPortCommunicate;

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.computemethods

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
  1. Create a newcalculatorByComputeFunctionMethod of usecomputecallbigComputeMethods:
Void calculatorByComputeFunction () is async {/ / using ` compute ` call ` bigCompute ` method, Int result = await compute(bigCompute, 0); print(result); }Copy the code
  1. Modify theFloatingActionButtonThe click event method of iscalculatorByComputeFunction
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’:.)

  1. To solveErrorWill:bigComputeInstead 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:

  1. 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.

  2. 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 processedMicroTask queueandEvent queueTasks in;
  • Event queueMainly handles external event tasks:I/O.Gesture events.The timer.Communication between isolatesAnd so on;
  • MicroTask queueMainly deal with internal tasks: such as processingI/OSome 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 firstMicroTask queueThe task whenMicroTask queueExecute the command only after the queue is emptyEvent queueTasks 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.

  1. 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.
  2. 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).
  3. 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;
  4. 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
  1. http.get('https://my.image.url')An unfinished state is returnedFutureCan be understood as a handle, andhttp.get('https://my.image.url')Was thrown into theEvent queueWait to be executed, and then proceed to execute the other current task;
  2. whenEvent queueSo after we execute thisgetA callback is performed when the request succeedsthenMethod to return the result,FutureTo complete, proceed with the next operation;
  3. whenEvent queueSo after we execute thisgetThe request is called back if it failscatchErrorMethod to return the error,FutureIs 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
  1. awaitPut the return value asFutureIn 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;
  2. Using theawaitMust be added after the methodasync;
  3. asyncMethods 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.