Understand dart asynchrony thoroughly

Time:2020-2-25

Fully understand DART’s asynchrony

Foreword 1: I will update some columns of flutter text tutorials in succession in the next period

Update progress:At least two articles per week;

Update location:The first time is in the official account, and the second day is updated in nuggets and places of thought.

More communication:You can add my wechat 372623326 and follow my micro blog: Code why

I hope you canHelp forward, click to seeGive me more creative power.

Foreword 2: before writing this article, I have been hesitating whether to explain DART’s asynchronous related topics here, because this part of the content is very easy for beginners to be deterred:

1. It’s easy to be confused about the relationship between single thread and asynchronous, although I will try my best to make you understand it in my own way.

2. A large number of asynchronous operation modes (future, await, async, etc.), currently you can’t see the specific application scenarios. (for example, if you have studied promise, await and async in the front end, it may be simpler, but I will assume that you do not have such a foundation.).

However, listen to me: if you have a lot of doubts after this chapter is finished, it doesn’t matter. When you use the relevant knowledge later, looking back, you will suddenly be enlightened.

1、 DART’s asynchronous model

Let’s figure out how dart handles 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 request, file reading and so on;
  • If our main thread has been waiting for these time-consuming operations to complete, it will block, unable to respond to other events, such as the user’s click;
  • Obviously, we can’t do this!!

How to deal with time-consuming operations?

  • Different languages have different ways to deal with time-consuming operations.
  • Treatment 1:Multithreading, such as Java and C + +, is a common practice for us to start a new thread, complete these asynchronous operations in the new thread, and then pass the data obtained to the main thread through the way of inter thread communication.
  • Treatment 2:Single thread + event loop, such as JavaScript and dart, is based on single thread plus event loop to complete the processing of time-consuming operations. But how can a single thread perform time-consuming operations?!

1.1.2. Asynchronous operation of single thread

I’ve met many developers who are full of questions about single thread asynchronous operations???

Understand dart asynchrony thoroughly

In fact, they do not conflict:

  • Because one of our applications is idle most of the time, not unlimited interaction with users.
  • For example, waiting for users to click, network request data return, file read-write IO operations, these waiting behaviors will not block our threads;
  • This is because similar to network request, file read-write IO, we can all call based on non blocking;

Blocking and non blocking calls

To understand this, we need to know theBlocking callandNonblocking callThe concept.

  • Blocking and non blocking concernsThe state of a program waiting for the result of a call (message, return value).
  • Blocking call:Before the call result returns, the current thread will be suspended, and the call thread will continue to execute only after getting the call result.
  • Non blocking call:After the call is executed, the current thread will not stop executing. It only needs a period of time to check whether there is any result returned.

Let’s use an example in life to simulate:

  • You’re hungry at noon. You need to order a take outTake out actionIt’s our call. Get itLast order takeoutIt’s the result we have to wait for.
  • Blocking call:Order takeout, no longer do anything, is in the silly waiting, your thread stopped any other work.
  • Non blocking call:After ordering takeout, continue to do other things: continue to work, play the game, your thread does not continue to perform other things, just need to occasionally see if there is a knock on the door, whether the takeout has been delivered.

Many time-consuming operations in our development can be based onNonblocking call

  • For example, the network request itself uses socket communication, while the socket itself provides a select model, which can be used toNon blocking operation
  • For example, we can use the IO operation provided by the operating system to read and write filesEvent based callback mechanism

These operations will not block our single thread’s continuous execution. Our thread can continue to do other things in the process of waiting: drink a cup of coffee, play a game, etc. when there is a real response, then go to the corresponding processing.

At this time, we may have two questions:

  • Question 1:If in multi-core CPU, does single thread not make full use of CPU? I will explain this problem later.
  • Question two:How does a single thread handle the results of network communication and IO operations? The answer is the event loop.

1.2. Dart event cycle

1.2.1. What is event cycle

In the single thread model, an event loop is maintained.

What is the event cycle?

  • In fact, the event loop is not complicated. It is to put a series of events (including click events, IO events, network events) to be handled in an event queue.
  • Take out the event from the event queue and execute the corresponding code block until the event queue is empty.

Let’s write a pseudo code for an event loop:

//Here I use array to simulate queue, first in, first out principle
List eventQueue = []; 
var event;

//The event loop is always executing from the moment it starts
while (true) {
  if (eventQueue.length > 0) {
    //Take out an event
    event = eventQueue.removeAt(0);
    //Execute the event
    event();
  }
}

When we have some events, such as click event, IO event and network event, they will be added to theeventLoopWhen the discovery event queue is not empty, the event will be fetched and executed.

  • The gear is our event loop, which takes events out of the queue once for execution.

Understand dart asynchrony thoroughly

1.2.2. Event cycle code simulation

Let’s take a look at a piece of pseudocode to understand how click events and network request events are executed:

  • This is a flutter code. You may not understand a lot of things, but read patiently and you will understand what we are doing.
  • A button raisedbutton that executes the onpressed function when a click occurs.
  • In the onpressed function, we send a network request, and the callback function in then will be executed after the request is successful.
RaisedButton(
  child: Text('Click me'),
  onPressed: () {
    final myFuture = http.get('https://example.com');
    myFuture.then((response) {
      if (response.statusCode == 200) {
        print('Success!');
      }
    });
  },
)

How is this code executed in an event loop?

  • 1. When the user clicks, the onpressed callback function is put into the event loop, and a network request is sent during the execution.
  • 2. After the network request is sent, the event loop will not be blocked, but the onppressed function to be executed is found to have ended and will be discarded.
  • 3. After the network request is successful, the callback function passed in then will be executed, which is also an event. This event will be put into the event loop for execution. After execution, the event loop will discard it.

Although there are some differences between the callbacks in onpressed and then, they tell the event loop:I have a piece of code to execute. Help me finish it quickly.

2、 Asynchronous operation of dart

The asynchronous operations in dart mainly use future, async and await.

If you have previous programming experience in ES6 and ES7 of the front end, you can fully understand future as promise. Async, await and ES7 are basically the same.

But without front-end development experience, how can future, async and await understand it?

2.1. Understanding future

I’ve been thinking for a long time about how to explain this future

2.1.1. Synchronous network request

Let’s take an example:

  • In this example, I use getnetworkdata to simulate a network request;
  • The network request takes 3 seconds, and then returns data;
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";
}

What is the result of this code running?

  • Getnetworkdata blocks the main function
main function start
//Wait 3 seconds
network data
main function end

Obviously, the above code is not the execution effect we want, because the network request blocks the main function, which means that all subsequent codes cannot continue to execute normally.

2.1.2. Asynchronous network request

Let’s improve the code above. The code is as follows:

  • The only difference between this code and the previous one is that I use the future object to put the time-consuming operation in the function passed in;
  • Later, we will explain some of its specific APIs. For the moment, we know that I have created a future instance;
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";
  });
}

Let’s take a look at the running results of the code:

  • 1. This time, the code is executed in sequence without any blocking;
  • 2. Different from the previous direct printing results, this time we printed a future example;
  • Conclusion: we isolate a time-consuming operation, which will no longer affect the execution of our main thread.
  • Question: how can we get the final result?
main function start
Instance of 'Future<String>'
main function end

Get the result of future

With future, how to get the requested result: through the callback of. Then:

main(List<String> args) {
  print("main function start");
  //Receive future returned by getnetworkdata with variable
  var future = getNetworkData();
  //When the future instance returns a result, it will automatically call back the function passed in from then
  //The function is placed in the event loop and executed
  future.then((value) {
    print(value);
  });
  print(future);
  print("main function end");
}

Execution result of the above code:

main function start
Instance of 'Future<String>'
main function end
//Execute the following code after 3S
network data

Exception in execution

If there is an exception in the calling process and no result can be obtained, how can the exception information be obtained?

import "dart:io";

main(List<String> args) {
  print("main function start");
  var future = getNetworkData();
  future.then((value) {
    print(value);
  }). Catchrror ((error) {// catch the exception
    print(error);
  });
  print(future);
  print("main function end");
}

Future<String> getNetworkData() {
  return Future<String>(() {
    sleep(Duration(seconds: 3));
    //No more results returned, but an exception occurred
    // return "network data";
    Throw exception ("network request error");
  });
}

Execution result of the above code:

main function start
Instance of 'Future<String>'
main function end
//We didn't get the result after 3S, but we caught the exception
Exception: network request error

2.1.3. Future use supplement

Supplement 1: summary of the above cases

We learned the use process of some future through a case:

  • 1. Create a future (it may be created by us, or it may be obtained by calling an internal API or a third-party API. In short, you need to obtain a future instance, which usually encapsulates some asynchronous operations);
  • 2. Through. Then (successful callback function) to monitor the results obtained when the internal execution of future is completed;
  • 3. Through. Catchrror (failure or exception callback function), listen for the error information of future internal execution failure or exception;

Supplement 2: two states of future

In fact, in the whole process of future execution, we usually divide it into two states:

Status one:Incomplete status

  • When performing the internal operations of future (in the above case, the specific network request process, we use the delay to simulate), we call this process the unfinished state

Status two:Completed status

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

Dart official website has the analysis of these two states. The reason why they are posted is that they are different from promise’s three states

Understand dart asynchrony thoroughly

Supplement 3: chain call of future

The above code can be improved as follows:

  • We can continue to return values in then, and we will get the returned results in the next chained then call callback function
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 more results returned, but an exception occurred
     return "network data1";
  });
}

The printing results are as follows:

main function start
main function end
//Get the result in 3S
network data1
content data2
message data3

Supplement IV: future other API

Future.value(value)

  • Get a completed future directly, which will directly call the callback function of then
main(List<String> args) {
  print("main function start");

  Future. Value ("hahaha"). Then ((value){
    print(value);
  });

  print("main function end");
}

The printing results are as follows:

main function start
main function end
Ha ha ha

Question: why immediately, butHa ha haWas it printed at the end?

  • This is because then in future will be added to the event queue as a new task. After joining, you must queue for execution

Future.error(object)

  • Get a completed future directly, but it is an abnormal future. The future will call the callback function of catchrror directly
main(List<String> args) {
  print("main function start");

  Future. Error (exception ("error message")). Catchrror ((error){
    print(error);
  });

  print("main function end");
}

The printing results are as follows:

main function start
main function end
Exception: error message

Future.delayed (time, callback function)

  • When the callback function is delayed for a certain time, the callback of then will be executed after the callback function is executed;
  • In the previous case, we can also use it to simulate, but directly learning this API will make you more confused;
main(List<String> args) {
  print("main function start");

  Future.delayed(Duration(seconds: 3), () {
    Return "information in 3 seconds";
  }).then((value) {
    print(value);
  });

  print("main function end");
}

2.2. await、async

2.2.1. Theoretical concept understanding

If you have a complete understanding of future, there should be no difficulty in learning await and async.

What are await and async?

  • They’re keywords in Dart (isn’t that bullshit? Nonsense is also important, in case you use it as variable name, innocent face.)
  • They allow us to useSynchronized code formatTo achieveAsynchronous calling procedure
  • And, usually, an async function will return a future (don’t worry, you’ll see the code soon).

We already know that future can do not block our thread, let the thread continue to execute, and change its state when completing an operation, and call back then or errorcatch.

How to generate a future?

  • 1. You can use the future constructor we learned earlier, or other future APIs we learned later.
  • 2. The other is through async.

2.2.2. Case code drill

Talk is cheap. Show me the code.

Let’s change the future asynchronous processing code to the form of await and async.

We know that if we write the code in this way, the code will not execute normally:

  • Because future.delayed returns a future object, we cannot treat it as synchronous return data:"network data"To use
  • That is to say, we can’t use this asynchronous code as synchronization!
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;
}

Now I use await to modify the following code:

  • You’ll find that I’mFuture.delayedThe function is preceded by an await.
  • Once you have this keyword, the operation will waitFuture.delayedAnd wait for its result.
String getNetworkData() {
  var result = await Future.delayed(Duration(seconds: 3), () {
    return "network data";
  });

  Return "requested data:" + result;
}

When executing the code after modification, you will see the following error:

  • The error is obvious: the await keyword must exist in the async function.
  • So we need togetNetworkDataFunction is defined as async function.

Understand dart asynchrony thoroughly

Continue to modify the code as follows:

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

  Return "requested data:" + result;
}

Run the code and still report an error (think: your sister ah):

  • The error is obvious: functions marked with async must return a future object.
  • So we need to continue to modify the code and write the return value as a future.

Understand dart asynchrony thoroughly

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;
}

This code should be the code we want to execute

  • We can now use the results returned asynchronously by future like synchronous code;
  • Wait for the result to be obtained and then splice it with other data, and then return together;
  • When returning, you do not need to wrap a future, just return directly, but the return value will be wrapped in a future by default;

2.3. Reading JSON cases

Here I give a case of reading a local JSON file, converting it into a model object and returning it in the flutter project;

This case serves as a reference for you to learn about future, await and async. I’m not going to talk about it, because I need to use relevant knowledge of flutter;

Later, I will explain it again in the following cases in the process of using it in flutter;

Read the JSON case code (just learn about it)

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 type
  final jsonResult = json.decode(jsonString);

  //3. Traverse the list, and turn it into an anchor object and put it in another list
  List<Anchor> anchors = new List();
  for (Map<String, dynamic> map in jsonResult) {
    anchors.add(Anchor.withMap(map));
  }
  return anchors;
}

3、 Asynchronous supplement of dart

3.1. Task execution sequence

3.1.1. Understanding micro task queue

In the previous learning, we know that there is an event loop in dart to execute our code, and there is an event queue in it. The event loop constantly takes events from the event queue for execution.

But if we strictly divide it, there is another queue in dart: Micro task queue.

  • The priority of micro task queue is higher than that of event queue;
  • In other wordsevent loopIt’s all priorityMicro task queueTasks in, execute againEvent queueTasks in;

So in the development of flutter, which is put in the event queue and which is put in the micro task queue?

  • All external event tasks are in the event queue, such as IO, timer, click, and draw events;
  • Micro tasks usually come from dart, and there are very few micro tasks. This is because if there are many micro tasks, the event queue will not be queued and the execution of the task queue will be blocked (for example, when the user clicks and does not respond);

Speaking of this, you may have been a bit messy. How does the code execute in DART’s single thread?

  • 1. DART’s entry is the main function, soCode in main functionWill give priority to implementation;
  • 2. After the main function is executed, an event loop will be started, and then the tasks in the queue will be executed;
  • 3. First, it will be executed in the order of first in, first outMicro task queueAll tasks in;
  • 4. Secondly, it will be executed in the order of first in, first outEvent queueAll tasks in;

Understand dart asynchrony thoroughly

3.1.2. how to create a micro task

In development, we can create a micro task through the schedule micro task under async in dart:

import "dart:async";

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

In development, if we have a task that we don’t want to queue in event queue, we can create a micro task.

Is the future code added to the event queue or the micro task queue?

There are usually two function executors in future:

  • Function body passed in by future constructor
  • The function body of then

So what queues do they join?

  • The function body passed in by the future constructor is placed in the event queue
  • The function bodies of then are divided into three cases:
  • Case 1: if future is not finished (there are tasks to be executed), then then they will be added directly to the function body of future;
  • Case 2: if the function body of the then is put into the micro task queue after the execution of future, the micro task queue will be executed after the execution of current future;
  • Case 3: if the future world chain call means that the next then will not be executed before the execution is completed;
//Future 1 is added to the eventqueue, followed by then 1
Future(() => print("future_1")).then((_) => print("then_1"));

//Future has no function executor and then 2 is added to microtaskqueue
Future(() => null).then((_) => print("then_2"));

//Future, then, and then are added to the eventqueue
Future(() => print("future_3")).then((_) => print("then_3_a")).then((_) => print("then_3_b"));

3.1.3. Code execution sequence

We learn one according to the previous rulesCode execution order of poleCase study:

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");
}

The result of code execution is:

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

Code analysis:

  • 1. The main function executes first, somain startandmain endExecute first, no problem;
  • 2. Main function executionIn process, some tasks will be added toEventQueueandMicrotaskQueueMedium;
  • 3. Task 7 passedscheduleMicrotaskFunction call, so it was first added to theMicrotaskQueue, will be executed first;
  • 4. Then start executionEventQueue, Task1 added toEventQueueIs implemented in;
  • 5. Passfinal future = Future(() => null);The future then created is added to the micro task, and the micro task is directly executed first, so task6 will be executed;
  • 6. Once in a whileEventQueueAdd task2, task3 and task5 to be executed;
  • 7. After the print execution of task3, callscheduleMicrotask, then after the execution of theEventQueueIt will be executed after task5, so task4 will be executed after task5 (Note:scheduleMicrotaskAs part of task3, task4 is executed after task5.)
  • 8. Task8, task9, task10 added toEventQueueBe executed;

In fact, the above code execution order may appear in the interview. This kind of complex nesting usually does not appear in our development, and we need to fully understand its execution order;

However, understanding the code execution sequence above will let youEventQueueandmicrotaskQueueHave a deeper understanding.

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 have known that dart is a single thread. This thread has its own memory space that can be accessed and event loops that need to be run;
  • We can call this space system an isolate;
  • For example, there is a root isolate in flutter, which is responsible for running flutter code, such as UI rendering, user interaction, etc;

In isolate, resource isolation is done very well. Each isolate has its own event loop and queue,

  • Isolates do not share any resources and can only communicate by message mechanism, so there is no problem of resource preemption.

However, if there is only one isolate, it means that we can only use one thread forever, which is a waste of resources for multi-core CPU.

If we have a lot of time-consuming calculations in development, we can create an isolate ourselves and complete the desired calculation operations in an independent isolate.

How to create an isolate?

It’s easy to create an isolate. We useIsolate.spawnYou can create:

import "dart:isolate";

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

void foo(info) {
  Print ("new isolate: $info");
}

3.2.2. Isolate communication mechanism

But in real development, we will not simply start a new isolate, and we will not care about its running results:

  • We need a new isolate to perform the calculation and inform the main isolate of the calculation results (that is, the default open isolate);
  • Isolate implements message communication mechanism through SendPort;
  • When starting concurrent isolate, we can pass the send pipeline of main isolate to it as a parameter;
  • At the end of execution, the pipeline can be used to send messages to main isolate;
import "dart:isolate";

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

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

  //3. Monitor pipeline messages
  receivePort.listen((data) {
    print('Data:$data');
    //When not in use, we close the pipe
    receivePort.close();
    //Need to kill isolate
    isolate?.kill(priority: Isolate.immediate);
  });
}

void foo(SendPort sendPort) {
  sendPort.send("Hello World");
}

But our above communication has become one-way communication. What if we need two-way communication?

  • In fact, two-way communication code will be more troublesome;
  • Flutter providescomputeFunction, which encapsulates the creation and bidirectional communication of isolate;
  • With it, we can make full use of multi-core CPU, and it is very simple to use;

Note: the following code is not DART’s API, but flutter’s API, so it can only be run in the flutter project

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

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

Note: all contents start with the official account. After that, Flutter will update other technical articles. TypeScript, React, Node, uniapp, mpvue, data structure and algorithm will also update some of their learning experiences. Welcome everyone’s attention.

Understand dart asynchrony thoroughly