This is the second article on learn to Build a Flutter App from Scratch.

This article addresses the question left over from the previous article: How does asynchronous processing work in Dart? We started with a brief overview of the asynchronous processing Future, Sync, and await commonly used in Dart. The second part attempts to analyze the asynchronous implementation principle of Dart as a single-threaded language, and further introduces IO model and event loop model. Finally, it describes how to implement multithreading to thread communication in Dart.

To make an asynchronous HTTP request, if you’re familiar with JavaScript’s Promise pattern, you could write:

new Promise((resolve, reject) =>{
    // Initiate a request
    const xhr = new XMLHttpRequest();
    xhr.open("GET".'https://www.nowait.xin/');
    xhr.onload = () => resolve(xhr.responseText); 
    xhr.onerror = () => reject(xhr.statusText);
    xhr.send();
}).then((response) => { / / success
   console.log(response);
}).catch((error) => { / / fail
   console.log(error);
});
Copy the code

Promise defines an asynchronous processing pattern: Do… success… or fail… .

In Dart, the equivalent is the Future object:

Future<Response> respFuture = http.get('https://example.com'); // Initiate a request
respFuture.then((response) { // Success, anonymous function
  if (response.statusCode == 200) {
    var data = reponse.data;
  }
}).catchError((error) { / / fail
   handle(error);
});
Copy the code

This mode simplifies and unified the asynchronous processing, even if there is no system to learn concurrent programming students, can also put away the complex multithreading, out of the box.

Future

The Future object encapsulates Dart’s asynchronous operations and has two states: uncompleted and completed.

In Dart, all IO functions are returned wrapped as Future objects. When you call an asynchronous function, you get an uncompleted state of the Future before the result or error returns.

Completed states also have two types. One indicates that the operation succeeded and returns a result. The other represents an operation that failed and returns an error.

Let’s look at an example:

Future<String> fetchUserOrder() {
  // Imagine a time-consuming database operation
  return Future(() => 'Large Latte');
}

void main() {
  fetchUserOrder().then((result){print(result)})
  print('Fetching user order... ');
}
Copy the code

Call the result back and forth through then, and main will print the result before the action in the Future:

Fetching user order...
Large Latte
Copy the code

In the example above, () => ‘Large Latte’) is an anonymous function, => ‘Large Latte’ is equivalent to return ‘Large Latte’.

Future’s eponymous constructor is Factory Future(FutureOr

Computation ()), whose function parameters return values of type FutureOr

, Future.then, Future.microTask also has parameters of type FutureOr

, so it’s worth learning about this object.


FutureOr

is a special type that has no class members, can’t be instantiated, and can’t be inherited. It looks like it’s probably just a syntactic sugar.

abstract class FutureOr<T> {
  // Private generative constructor, so that it is not subclassable, mixable, or
  // instantiable.
  FutureOr._() {
    throw new UnsupportedError("FutureOr can't be instantiated"); }}Copy the code

You can think of it as a limited dynamic because it can only accept values of type Future

or T:

FutureOr<int> hello(){}

void main(){
   FutureOr<int> a = 1; //OK
   FutureOr<int> b = Future.value(1); //OK
   FutureOr<int> aa = '1' // Error compiling

   int c = hello(); //ok
   Future<int> cc = hello(); //ok
   String s = hello(); // Error compiling
}
Copy the code

Dart best practice explicitly states: Avoid declaring a function return type FutureOr

.

If you call the following function, there is no way to know whether the return value is of type int or Future

unless you enter the source code:

FutureOr<int> triple(FutureOr<int> value) async= > (await value) * 3;
Copy the code

Correct way to write it:

Future<int> triple(FutureOr<int> value) async= > (await value) * 3;
Copy the code

With a little explanation of FutureOr

, let’s move on to Future.

If an exception occurs in the execution of a function within the Future, you can use future.catcherror to handle the exception:

Future<void> fetchUserOrder() {
  return Future.delayed(Duration(seconds: 3), () = >throw Exception('Logout failed: user ID is invalid'));
}

void main() {
  fetchUserOrder().catchError((err, s){print(err); });print('Fetching user order... ');
}
Copy the code

Output result:

Fetching user order...
Exception: Logout failed: user ID is invalid
Copy the code

Future supports chained calls:

Future<String> fetchUserOrder() {
  return Future(() => 'AAA');
}

void main() {
   fetchUserOrder().then((result) => result + 'BBB')
     .then((result) => result + 'CCC')
     .then((result){print(result); }); }Copy the code

Output result:

AAABBBCCC
Copy the code

Async and await

Imagine a scenario like this:

  1. First call the login interface;
  2. Obtain user information based on the token returned by the login interface.
  3. Finally, the user information is cached to the local machine.

Interface definition:

Future<String> login(String name,String password){
  / / login
}
Future<User> fetchUserInfo(String token){
  // Get user information
}
Future saveUserInfo(User user){
  // Cache user information
}
Copy the code

Future:

login('name'.'password').then((token) => fetchUserInfo(token))
  .then((user) => saveUserInfo(user));
Copy the code

Instead of async and await, we can do this:

void doLogin() async {
  String token = await login('name'.'password'); //await must be inside async function
  User user = await fetchUserInfo(token);
  await saveUserInfo(user);
}
Copy the code

A function that declares async and returns a Future object. Even if you return type T directly in async, the compiler will automatically wrap it for you as a Future

object, or a Future

object if it is a void function. The Futrue type will be unwrapped and the original datatype will be exposed again when we get await. Note that the async keyword must be added to the function with await.

Await code exception, caught in the same way as synchronous call function:

void doLogin() async {
  try {
    var token = await login('name'.'password');
    var user = await fetchUserInfo(token);
    await saveUserInfo(user);
  } catch (err) {
    print('Caught error: $err'); }}Copy the code

Thanks to async and await syntactic candy, you can treat asynchronous code with synchronous programming thinking, greatly simplifying the processing of asynchronous code.

Note: There’s a lot of syntax sugar in Dart, which makes our programming more efficient, but it can also be confusing for beginners.

Give you an extra grammar candy:

Future<String> getUserInfo() async {
  return 'aaa'; } is equivalent to: Future<String> getUserInfo() async {
  return Future.value('aaa');
}
Copy the code

Dart Asynchronous principle

Dart is a single-threaded programming language. For those of you who usually use Java, the first reaction may be: If an operation takes too long, won’t the main thread stay stuck? On Android, in order not to block the main UI thread, we have to use another thread to initiate time-consuming operations (network requests/access to local files, etc.) and then use a Handler to communicate with the UI thread. How does Dart do it?

Asynchronous IO + event loop The following is specific analysis.

I/O model

Let’s first look at what blocking IO looks like:

int count = io.read(buffer); // block wait
Copy the code

Note: the IO model is operating system level, this section of the code is pseudo-code, just for ease of understanding.

When the thread calls read, it sits there waiting for the result to return, doing nothing. This is blocking IO.

However, our application often needs to process several IO at the same time. Even for a simple mobile App, IO may occur simultaneously: user gestures (input), several network requests (input and output), rendering results to the screen (output); Not to mention server applications, where hundreds or thousands of concurrent requests are common.

Somebody said, you can use multiple threads in this case. This is an idea, but given the actual number of concurrent CPU processes, each thread can only handle a single IO at the same time, which is still very limited in performance, and also has to deal with synchronization between different threads, which increases the complexity of the program greatly.

If I/O does not block, the situation is different:

while(true) {for(io in io_array){
      status = io.read(buffer);// Return immediately with or without data
      if(status == OK){
       
      }
  }
}
Copy the code

A non-blocking IO, through the way of polling, we can on multiple IO dealt with at the same time, but it also has an obvious shortcoming: in most cases, IO is no content (CPU speed is much higher than IO), it will cause the CPU in idle most of the time, computing resources is still not very good.

To further solve this problem, IO multiplexing was designed to listen on multiple I/OS and set the wait time:

while(true) {// If data is returned from one IO, it is returned immediately. If no, the waiting time does not exceed the timeout period
    status = select(io_array, timeout); 
    if(status  == OK){
      for(io in io_array){
          io.read() // Return immediately, the data is ready}}}Copy the code

There are various implementations of I/O multiplexing, such as SELECT, poll, and epoll.

With IO multiplexing, CPU utilization efficiency is improved.

For those of you with a keen eye, you may have noticed that in the above code, threads can still block on the select or cause some idling. Is there a more perfect solution?

The answer is asynchronous IO:

io.async_read((data) => {
  // dosomething
});
Copy the code

With asynchronous IO, we don’t have to keep asking the operating system: Are you ready for the data? Instead, the system sends us a message or callback as soon as it has data. This may seem like a good idea, but unfortunately, not all operating systems support this feature well. For example, Linux has a lot of drawbacks with asynchronous IO, so there are often compromises in the implementation of asynchronous IO, such as the libeio library behind Node.js. Essentially asynchronous I/O simulated by thread pools and blocking I/O [1].

Dart is also mentioned in the documentation as an asynchronous IO implemented by Node.js, EventMachine, and Twisted, but we won’t dig into its internal implementation. Found on Android and Linux that seems to be done via epoll), in the Dart layer we just think of IO as asynchronous.

Let’s go back to the Future code above:

Future<Response> respFuture = http.get('https://example.com'); // Initiate a request
Copy the code

Now, you know, this network request is not done on the main thread, it actually leaves the job to the runtime or the operating system. This is why Dart, as a single-process language, does IO operations without blocking the main thread.

We’ve solved the problem with the Dart single thread I/O, but how does the main thread deal with large numbers of asynchronous messages? Let’s move on to Dart’s Event Loop.

Event Loop

In Dart, each thread runs in a separate environment called ISOLATE, its memory is not shared with other threads, and it is constantly doing one thing: pulling events from the event queue and processing them.

while(true){
   event = event_queue.first() // Retrieve the event
   handleEvent(event) // Handle events
   drop(event) // Remove from queue
}
Copy the code

Take this code for example:

RaisedButton(
  child: Text('click me');
  onPressed: (){ // Click the event
     Future<Response> respFuture = http.get('https://example.com'); 
     respFuture.then((response){ // IO returns an event
        if(response.statusCode == 200) {print('success'); }})})Copy the code

When you click a button on the screen, an event is generated, and this event is placed in the event queue of the ISOLATE. Then you make a network request, which also generates an event, which goes into a loop of events.

When threads are idle, the ISOLATE can also do garbage collection (GC) and have a cup of coffee.

The API layer’s Future, Stream, Async, and await are really abstractions of event loops at the code layer. In combination with the event loop, it’s probably best to return to the definition of An object representing a delayed computation. Eldest brother isolate, I will express a code package to you. After you get it, open the box and execute the codes in sequence.

In fact, there are two queues in THE ISOLATE, one is the event queue and the other is the Microtask queue.

Event queue: Used to process external events, such as IO, click, draw, timer, message events between different isolates, etc.

Microtask queue: Processes tasks from within the Dart and is suitable for tasks that are not particularly time-consuming or urgent. Microtask queues have a higher processing priority than event queues. If microtask processing is time-consuming, events will accumulate and application response will be slow.

You can submit a microtask to isolate using future. microtask:

import 'dart:async';

main() {
  new Future(() => print('beautiful'));
  Future.microtask(() => print('hi'));
}
Copy the code

Output:

hi
beautiful
Copy the code

To summarize how the event loop works: When an application starts, it creates an isolate, starts the event loop, processes the microtask queue first, then the event queue, and so on, in FIFO order.

multithreading

Note: When we talk about isolate below, you can equate it with threads, but we know it’s not just a thread.

Even though Dart is single-threaded, the average IO intensive App usually gets excellent performance thanks to asynchronous IO + event loops. But for some computationally intensive scenarios, such as image processing, deserialization, and file compression, a single thread is not sufficient.

In Dart, you can create a new Isolate using isolate. spawn:

void newIsolate(String mainMessage){
  sleep(Duration(seconds: 3));
  print(mainMessage);
}

void main() {
  Create a new ISOLATE, newIoslate
  Isolate.spawn(newIsolate, 'Hello, Im from new isolate! '); 
  sleep(Duration(seconds: 10)); // The main thread is blocked and waiting
}
Copy the code

Output:

Hello, Im from new isolate!
Copy the code

Spawn takes two mandatory arguments. The first is the entrypoint of the new ISOLATE and the second is the value of the argument to the spawn function.

If the main ISOLATE wants to receive messages from its sub-isolate, you can create a ReceivePort object on the main ISOLATE and pass in the corresponding receivePort.sendPort as a parameter to the new isolate entry function. Then send a message to the main ISOLATE using the ReceivePort binding SendPort object:

// New ISOLATE entry functions
void newIsolate(SendPort sendPort){
  sendPort.send("hello, Im from new isolate!");
}

void main() async{
  ReceivePort receivePort= ReceivePort();
  Isolate isolate = await Isolate.spawn(newIsolate, receivePort.sendPort);
  receivePort.listen((message){ // Listen for messages sent from the new ISOLATE
   
    print(message);
     
    // Close the pipe when it is no longer in use
     receivePort.close();
     
    // Disable the ISOLATE threadisolate? .kill(priority: Isolate.immediate); }); }Copy the code

Output:

hello, Im from new isolate!
Copy the code

We have seen how the main isolate listens for messages from the sub-isolate. What can I do if the sub-isolate also wants to know the status of the main isolate? The following code provides a way to communicate both ways:

Future<SendPort> initIsolate() async {
  Completer completer = new Completer<SendPort>();
  ReceivePort isolateToMainStream = ReceivePort();

  // Listen for messages from child threads
  isolateToMainStream.listen((data) {
    if (data is SendPort) {
      SendPort mainToIsolateStream = data;
      completer.complete(mainToIsolateStream);
    } else {
      print('[isolateToMainStream] $data'); }}); Isolate myIsolateInstance =await Isolate.spawn(newIsolate, isolateToMainStream.sendPort);
  // Return sendPort from the subisolate
  return completer.future; 
}

void newIsolate(SendPort isolateToMainStream) {
  ReceivePort mainToIsolateStream = ReceivePort();
  Key implementation: Return SendPort to the main ISOLATE
  isolateToMainStream.send(mainToIsolateStream.sendPort);

  // Listen for messages to isolate autonomously
  mainToIsolateStream.listen((data) {
    print('[mainToIsolateStream] $data');
  });

  isolateToMainStream.send('This is from new isolate');
}

void main() async{
  SendPort mainToIsolate = await initIsolate();
  mainToIsolate.send('This is from main isolate');
}
Copy the code

Output:

[mainToIsolateStream] This is from main isolatemain end
[isolateToMainStream] This is from new isolate
Copy the code

In Flutter, you can also start a new ISOLATE with a simplified version of compute.

For example, in a deserialization scenario, serialization is done directly on the main ISOLATE:

List<Photo> parsePhotos(String responseBody) {
  final parsed = json.decode(responseBody).cast<Map<String.dynamic> > ();return parsed.map<Photo>((json) => Photo.fromJson(json)).toList();
}

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response =
      await client.get('https://jsonplaceholder.typicode.com/photos');
  // Switch directly on the main ISOLATE
  return parsePhotos(response.body); 
}
Copy the code

Start a new ISOLATE:

Future<List<Photo>> fetchPhotos(http.Client client) async {
  final response =
      await client.get('https://jsonplaceholder.typicode.com/photos');
  // Start a new ISOLATE using the compute function
  return compute(parsePhotos, response.body);
}
Copy the code

Full version of this example: Parse JSON in the background

To summarize, when you run into computationally intensive, time-consuming operations, you can start a new ISOLATE to perform tasks concurrently. Unlike multithreading, different isolates cannot share memory. However, message channels between different isolates can be established through ReceivePort and SendPort. In addition, messages from other isolates also pass through an event loop.

The resources

  1. Dart asynchronous programming: isolate and event loops
  2. The Event Loop and Dart
  3. Asynchronous I/O implementation of Node.js
  4. Dart Isolate 2-Way Communication
  5. Thoroughly understand Dart asynchrony

aboutAgileStudio

We are a team of experienced independent developers and designers with solid technical strength and years of product design and development experience, providing reliable software customization services.