I will update a series of Flutter text tutorials in the coming days

Update progress: at least two articles per week;

Update location: first in the public account, the next day update in nuggets, Sifu and other places;

More communication: You can add me on wechat at 372623326 and follow me on Weibo at CoderWhy

I hope you can help forward, click to see, give me more creative power.

Before writing this article, I was hesitant to cover the topic of Dart asynchrony here, as it can easily be daunting for beginners:

1. The relationship between single threading and asynchrony is a bit confusing, although I will try to make it as clear as POSSIBLE in my own way.

2, a lot of asynchronous operations (Future, await, async, etc.), so far you can’t see the specific application scenarios. (If you’ve learned Promise, await, async in the front end, it might be easy, but I’m going to assume you don’t have that foundation).

But listen to me: If you still have a lot of doubts after this chapter, that’s ok. When you come back to use the relevant knowledge, you’ll see things in retrospect.

Dart asynchronous model

How does Dart handle asynchronous operations

1.1. Dart is single-threaded

1.1.1. Time-consuming operations in the program

Time-consuming operations in development:

  • 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. But how can a single thread perform time-consuming operations? !

1.1.2. Single-threaded asynchronous operations

I’ve met a lot of developers who are skeptical about single-threaded asynchronous operations.

They don’t conflict:

  • Because one of our applications is idle most of the time, there is no unlimited interaction with the user.
  • For example, waiting for a user to click, the return of network request data, or the IO operation of a file read or write does not block our thread;
  • This is because IO, such as network requests and file reads and writes, can be called on a non-blocking basis;

Blocking and non-blocking calls

To understand this, we need to understand the concept of blocking and non-blocking invocation in operating systems.

  • Blocking and non-blocking are concerned with the state of the program while it waits for the result of the call (message, return value).
  • 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.

Let’s use a real-life example:

  • You’re hungry for lunch. You need to order takeout. OrderThe act of take-outThat’s our call, getLast takeout orderThat’s what we’re waiting for.
  • Blocking calls: ordering takeout and not doing anything is just waiting stupidly for your thread to stop doing anything else.
  • Non-blocking calls: You order a takeaway and continue to do something else: you continue to work, play a game, and your thread is not doing anything else, just checking occasionally to see if there’s a knock on the door or if the takeaway has arrived.

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 the Socket itself provides a SELECT model, which can be carried outWorks in a non-blocking manner;
  • For example, file read and write IO operations, we can use the operating system providedEvent-based callback mechanism;

None of these actions will block our single-thread execution, and our thread can continue to do other things while it waits: grab a cup of coffee, play a game, and wait for the actual response to be processed.

At this point, we may have two questions:

  • Question 1: If you have a multi-core CPU, does a single thread underuse the CPU? I’ll talk about that later.
  • Q2: How does a single thread process the results of network traffic, IO operations and so on? The answer is the Event Loop.

1.2. Dart Event loop

1.2.1. What is an event loop

The single-threaded model basically maintains an Event Loop.

What is the event loop?

  • In fact, the Event loop is not complicated, it is a series of events (including click events, IO events, network events) to be processed in an Event Queue.
  • Continually fetching events from the Event Queue and executing their corresponding blocks of code until the Event Queue is empty.

Let’s write pseudocode for an event loop:

// Here I use arrays to simulate queues, first in, first out
List eventQueue = []; 
var event;

// The event loop is always executed from the moment it starts
while (true) {
  if (eventQueue.length > 0) {
    // Retrieve an event
    event = eventQueue.removeAt(0);
    // Execute the eventevent(); }}Copy the code

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

  • The gear is our event loop, pulling events out of the queue one at a time to execute.

1.2.2. Event loop code simulation

Here’s a piece of pseudocode to understand how click events and network request events are executed:

  • This is the Flutter code, many things you may not understand, but with patience you will understand what we are doing.
  • A button, RaisedButton, executes the onPressed function when clicked.
  • In the onPressed function, we send a network request and execute the callback function in THEN when the request succeeds.
RaisedButton(
  child: Text('Click me'),
  onPressed: () {
    final myFuture = http.get('https://example.com');
    myFuture.then((response) {
      if (response.statusCode == 200) {
        print('Success! '); }}); },)Copy the code

How is this code executed in an event loop?

  • 1. When the user clicks, the onPressed callback is placed into an event loop and a network request is sent during execution.
  • 2. Instead of blocking after the network request is sent, the event loop is discarded after the onPressed function is finished.
  • 3. After the network request is successful, the callback function passed in THEN is executed, which is also an event. The event is put into the event loop, and the event loop discards it after execution.

While onPressed is a little different than the callback in THEN, they both tell the event loop that I have a piece of code that needs to be executed, help me get it done.

Dart asynchronous operation

Asynchronous operations in Dart use futures and async and await.

If you have experience with front-end ES6 and ES7 programming, Future can be interpreted as Promise, async and await are basically the same as in ES7.

But what about Future and async and await without front-end development experience?

2.1 to know the Future

I have been thinking for a long time. How should the Future be explained

2.1.1. Synchronous network requests

Let’s start with an example:

  • In this example, I use getNetworkData to simulate a network request;
  • The network request takes three seconds, after which the data is returned;
import "dart:io";

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

String getNetworkData() {
  sleep(Duration(seconds: 3));
  return "network data";
}
Copy the code

What does this code run like?

  • GetNetworkData blocks execution of main
main function start
// Wait 3 seconds
network data
main function end
Copy the code

Obviously, the above code is not the desired execution, because the network request blocks the main function, which means that all subsequent code cannot continue to execute properly.

2.1.2. Asynchronous network requests

Let’s improve our above code, the code is as follows:

  • The only difference is that I use a Future object to place time-consuming operations in the function passed in;
  • We’ll talk about some of the apis in a minute, but we’ll just know for a moment that I created an instance of a Future;
import "dart:io";

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

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    return "network data";
  });
}
Copy the code

Let’s look at the result of this code:

  • 1. The code was executed sequentially without any blocking phenomenon;
  • 2. Instead of printing the result directly, this time we print an instance of the Future;
  • Conclusion: We have isolated a time-consuming operation that will no longer affect our main thread execution.
  • Question: How do we get the final result?
main function start
Instance of 'Future<String>'
main function end
Copy the code

Get the result of the Future

With a Future, how to get the result of a request: via a callback to.then:

main(List<String> args) {
  print("main function start");
  // Use the variable to receive the future returned by getNetworkData
  var future = getNetworkData();
  // When a future instance returns a result, the function passed in then is automatically called
  // This function is put into the event loop and executed
  future.then((value) {
    print(value);
  });
  print(future);
  print("main function end");
}
Copy the code

The result of executing the above code:

main function start
Instance of 'Future<String>'
main function end
// execute the following code after 3s
network data
Copy the code

An exception occurred during execution

If an exception occurs during the call and the result cannot be obtained, how can I obtain the information about the exception?

import "dart:io";

main(List<String> args) {
  print("main function start");
  var future = getNetworkData();
  future.then((value) {
    print(value);
  }).catchError((error) { // Catch the case when an exception occurs
    print(error);
  });
  print(future);
  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    // No longer returns a result, but an exception
    // return "network data";
    throw Exception("Network request error");
  });
}

Copy the code

The result of executing the above code:

main function start
Instance of 'Future<String>'
main function end
// We did not get the result after 3s, but we caught the exceptionException: Network request errorCopy the code

2.1.3. Future uses supplements

Supplement 1: summary of the case above

We use a case to learn some Future usage process:

  • Create a Future (either by us, or by calling an internal API or a third-party API, you need to get an instance of a Future, which usually encapsulates asynchronous operations);
  • 2. Use.then(success callback) to listen for the result of the Future’s internal execution.
  • CatchError messages from Future catchError via.catcherror

Supplement two: The two states of the Future

In fact, during the entire implementation of a Future, we usually divide it into two states:

State 1: Uncompleted State

  • When an operation inside a Future is performed (in the case above, the specific network request process, which we simulated with a delay), we call it an incomplete state

State 2: Completed State

  • When the operation inside the Future completes, it usually returns a value or throws an exception.
  • In both cases, we call the Future a completed state.

Dart’s official website analyzes these two states, which are different from Promise’s three states

Add 3: Chain calls to Future

We can make the following improvements to the above code:

  • We can continue to return values in THEN and get the result in the next chained THEN call callback
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);
  });

  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    // No longer returns a result, but an exception
     return "network data1";
  });
}

Copy the code

The print result is as follows:

main function start
main function end
// get the result after 3 seconds
network data1
content data2
message data3

Copy the code

Add 4: Future other apis

Future.value(value)

  • Gets a completed Future directly, which calls the then callback function directly
main(List<String> args) {
  print("main function start");

  Future.value("Ha ha ha.").then((value) {
    print(value);
  });

  print("main function end");
}

Copy the code

The print result is as follows:

Main function start main function end hahahaCopy the code

Confused: why execute immediately, but hahaha is printed last?

  • This is because the “Then” in the Future will be added to the Event Queue as a new task, and you will need to Queue up to execute it

Future.error(object)

  • Get a completed Future directly, but with an exception, that calls catchError’s callback directly
main(List<String> args) {
  print("main function start");

  Future.error(Exception("Error message")).catchError((error) {
    print(error);
  });

  print("main function end");
}

Copy the code

The print result is as follows:

Main function start Main function end Exception: error messageCopy the code

Future.delayed(time, callback function)

  • The callback function is executed when the delay is certain, and then callback is executed after the callback function is executed.
  • In the previous example, we could have used it to simulate, but learning the API directly would have been a little bit more confusing;
main(List<String> args) {
  print("main function start");

  Future.delayed(Duration(seconds: 3), () {
    return "Message in three seconds.";
  }).then((value) {
    print(value);
  });

  print("main function end");
}

Copy the code

2.2. Await, async

2.2.1. Understanding of theoretical concepts

If you already understand Future completely, it should be easy to learn await and async.

What is await, async?

  • They are keywords in Dart. Nonsense is also emphasized, in case you use it as a variable name, innocent face.
  • They can be used by usSynchronized code formatTo achieveAsynchronous call process.
  • Also, usually an async function returns a Future (don’t worry, you’ll see the code in a minute).

We already know that a Future can do this without blocking our thread, allowing it to continue executing, change its state when it completes an operation, and call back then or errorCatch callbacks.

How do you generate a Future?

  • 1. Use the Future constructor we learned earlier, or any Future API we learned later.
  • 2. Another way is through async functions.

2.2.2. Case code drill

Talk is cheap. Show me the code.

Let’s modify our Future asynchronous processing code to await and async.

We know that the code will not execute if we write it directly like this:

  • Because future.delayed returns a Future object, we cannot treat it as synchronous returned data:"network data"To use the
  • We cannot use asynchronous code as if it were synchronous!
import "dart:io";

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

String getNetworkData() {
  var result = Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return  "Requested data:" + result;
}

Copy the code

I now modify the following line of code with await:

  • You will find that I amFuture.delayedThe function is preceded by an await.
  • Once the keyword is present, the operation will waitFuture.delayed“, and waits for its results.
String getNetworkData() {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return  "Requested data:" + result;
}

Copy the code

After executing the code, you will see the following error:

  • The error is obvious: the await keyword must exist in the async function.
  • So we need to put thegetNetworkDataFunctions are defined as async functions.

Continue to modify the code as follows:

  • It’s as simple as adding an async keyword after the () of a function
String getNetworkData() async {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return  "Requested data:" + result;
}

Copy the code

Run code, still error (thinking: your sister) :

  • The error is obvious: functions that use the async tag must return a Future object.
  • So we need to keep modifying the code and write the return value as a Future.

Continue to modify the code as follows:

Future<String> getNetworkData() async {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  return "Requested data:" + result;
}

Copy the code

This code should be the code we want to execute

  • We can now use the result returned asynchronously by the Future as if it were synchronous code;
  • Wait for the results to be joined with other data, and then return together;
  • When you return, you don’t need to wrap a Future, you just return it, but the return value is wrapped in a Future by default.

2.3. Read the JSON case

Here I give an example of a Flutter project that reads a local JSON file, converts it to a model object, and returns it.

This case is a reference for you to learn about Future and await and async. I am not going to expand it because I need to know about Flutter.

I’ll talk about it again in a later case in my use of Flutter;

Read the JSON case code

import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';

main(List<String> args) {
  getAnchors().then((anchors) {
    print(anchors);
  });
}

class Anchor {
  String nickname;
  String roomName;
  String imageUrl;

  Anchor({
    this.nickname,
    this.roomName,
    this.imageUrl
  });

  Anchor.withMap(Map<String.dynamic> parsedMap) {
    this.nickname = parsedMap["nickname"];
    this.roomName = parsedMap["roomName"];
    this.imageUrl = parsedMap["roomSrc"];
  }
}

Future<List<Anchor>> getAnchors() async {
  // 1. Read the JSON file
  String jsonString = await rootBundle.loadString("assets/yz.json");

  // 2. Convert to List or Map
  final jsonResult = json.decode(jsonString);

  // 3. Iterate through the List and cast the Anchor object into another List
  List<Anchor> anchors = new List(a);for (Map<String.dynamic> map in jsonResult) {
    anchors.add(Anchor.withMap(map));
  }
  return anchors;
}

Copy the code

Asynchronous complementation of Dart

3.1. Task execution sequence

3.1.1. Understanding microtask queues

We learned that Dart has an Event Loop to execute our code, an Event Queue, and the Event Loop keeps pulling events from the Event Queue.

But there is another Queue in Dart, if we break it down strictly: the Microtask Queue.

  • The priority of microtask queue is higher than that of event queue.
  • That is to say,Event loopIt’s all a priorityMicrotask queueAnd then execute the taskThe event queueTask in;

So in Flutter development, which should be placed in the event queue and which should be placed in the microtask queue?

  • All external event tasks are in the event queue, such as IO, timer, click, and draw events.
  • Microtasks are often derived from within Dart and are few and far between. This is because if there are too many micro-tasks, the event queue will not be queued up and the execution of the task queue will be blocked (for example, when the user clicks and does not respond).

You might be getting a little messy here, but how does code actually execute in a single-threaded Dart?

  • Dart’s entry is the main function, soCode in the main functionWill take precedence;
  • 2. After the main function is executed, an Event Loop will be started and the tasks in the queue will be executed.
  • 3. First of all, it will be executed in a first-in, first-out orderMicrotask QueueAll tasks in;
  • 4. Secondly, it will be executed in a first-in, first-out orderEvent QueueAll tasks in;

3.1.2. How do I create microtasks

In development, we can create a microtask using scheduleMicrotask under Async in DART:

import "dart:async";

main(List<String> args) {
  scheduleMicrotask(() {
    print("Hello Microtask");
  });
}
Copy the code

In development, if we have a task that we don’t want queued in the Event Queue, we can create a microtask.

Will the Future’s code join the event queue or the microtask queue?

There are usually two function bodies in a Future:

  • The body of the function passed in by the Future constructor
  • The body of the function then (catchError)

So what queue are they joining?

  • The body of the function passed in by the Future constructor is placed in the event queue
  • The function body of then is divided into three cases:
  • If the Future is not executed, then is added directly to the function body of the Future.
  • If the Future is then executed, the body of the then function is put into the microtask queue, and the microtask queue is executed after the current Future is executed.
  • If the Future chain is called, it means that the next THEN will not be executed.
// future_1 is added to the eventQueue, followed by THEN_1 to the eventQueue
Future(() => print("future_1")).then((_) => print("then_1"));

// The Future has no function body, then_2 is added to microTaskQueue
Future(() => null).then((_) => print("then_2"));

// Future_3, THEN_3_A, and THEN_3_B are added to the eventQueue in sequence
Future(() => print("future_3")).then((_) => print("then_3_a")).then((_) => print("then_3_b"));
Copy the code

3.1.3. Code execution sequence

Let’s look at an example of a polar code execution sequence based on the previous rule:

import "dart:async";

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

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

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

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

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

  print("main end");
}

Copy the code

The result of code execution is:

main start
main end
task7
task1
task6
task2
task3
task5
task4
task8
task9
task10

Copy the code

Code analysis:

  • 1. Main is executed first, somain startandmain endExecute first, no problem;
  • 2. Execute the main functionIn the process of, will add some tasks separately toEventQueueandMicrotaskQueue;
  • 3. Task7 passesscheduleMicrotaskFunction call, so it was first added toMicrotaskQueue, will be executed first;
  • 4. Then executeEventQueue, task1 is added toEventQueueTo be executed;
  • 5, throughfinal future = Future(() => null);The then of the created future is added to the micro-task. The micro-task is executed first, so task6 is executed.
  • 6, once inEventQueueTask2, task3, task5 are executed.
  • 7. Call task3 when the print is completescheduleMicrotaskSo after the execution of thisEventQueueTask4 is executed after task5.scheduleMicrotaskThe call to task3 is part of the code, so task4 is executed after Task5.
  • 8, task8, task9, task10 add toEventQueueBe performed;

In fact, the order in which the code is executed is likely to appear in an interview. We don’t usually have this kind of complex nesting in development and need to be completely clear about the order;

However, understanding the sequence of code execution above will give you a better understanding of EventQueue and microtaskQueue.

3.2. Utilization of multi-core CPU

3.2.1. Understanding of Isolate

In Dart, there is a concept of Isolate. What is it?

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

How do you create an Isolate?

Creating isolate. spawn is relatively easy, we can create it using isolate. spawn:

import "dart:isolate";

main(List<String> args) {
  Isolate.spawn(foo, "Hello Isolate");
}

void foo(info) {
  print("New ISOLATE:$info");
}

Copy the code

3.2.2. Isolate Communication mechanism

But in real development, we didn’t just start a new Isolate and not care about the results:

  • We need the new Isolate to calculate and inform the Main Isolate (the Isolate enabled by default) of the results.
  • The Isolate uses SendPort to communicate messages.
  • We can pass the Main Isolate’s send channel as a parameter when the Isolate is started and concurrency occurs;
  • Concurrency when execution is complete, this pipe can be used to send messages to the Main Isolate;
import "dart:isolate";

main(List<String> args) async {
  // 1. Create a pipe
  ReceivePort receivePort= ReceivePort();

  // 2. Create an Isolate
  Isolate isolate = await Isolate.spawn<SendPort>(foo, receivePort.sendPort);

  // 3. Listen for pipe messages
  receivePort.listen((data) {
    print('Data:$data');
    // We close the pipe when it is no longer in use
    receivePort.close();
    // Kill the ISOLATEisolate? .kill(priority: Isolate.immediate); }); }void foo(SendPort sendPort) {
  sendPort.send("Hello World");
}

Copy the code

But our communication above becomes one-way communication, what if we need two-way communication?

  • In fact the code for two-way communication is a bit more cumbersome;
  • Flutter provides support for concurrent computationcomputeFunction, which internally encapsulates the creation and two-way communication of the Isolate;
  • It allows us to take full advantage of multi-core cpus and is very simple to use.

Note: The code below is not the DART API, but the Flutter API, so it only runs in the Flutter project

main(List<String> args) async {
  int result = await compute(powerNum, 5);
  print(result);
}

int powerNum(int num) {
  return num * num;
}

Copy the code

Note: All content will be published on our official website. Later, Flutter will also update other technical articles, including TypeScript, React, Node, Uniapp, MPvue, data structures and algorithms, etc. We will also update some of our own learning experiences