Fluent - dart event loop mechanism and asynchronous

Posted by zeth369 on Tue, 08 Mar 2022 12:24:36 +0100

Welcome to WeChat official account: FSA full stack operation 👋

1, Dart asynchronous

Like JavaScript, Dart is a single thread model based on event loop mechanism, so there is no multithreading in Dart, so there is no distinction between main thread and sub thread

1. Synchronous and asynchronous

  • Synchronization: in the same thread, execute from top to bottom according to the code writing order (intuitive feeling: need to wait)
  • Asynchronous: during code execution, the execution of a certain section of code will not affect the execution of subsequent codes (intuitive feeling: no need to wait)

2. Single thread model

Single threaded model:

  • On an execution line, only one task (event) can be executed at the same time, and other tasks must queue up behind to be executed
  • In order not to hinder the execution of the code, every time-consuming task encountered will be suspended and put into the task queue. After the execution is completed, the tasks on the queue will be executed in order, so as to achieve the asynchronous effect

Advantages of single thread model and multi thread model:

  • Advantages of single thread model: it avoids the disadvantages of multithreading and is more suitable for time-consuming operations that need to wait for the other party to transmit data or return results, such as network request, IO operation, etc
  • The advantage of multithreading: make use of the multi-core of the processor to realize the computing intensive operation of parallel computing

Disadvantages of multithreading:

  • It will bring additional resource and performance consumption
  • Lock control is required when multiple threads operate shared memory. Lock competition will reduce performance and efficiency, and it is easy to cause deadlock in complex situations

3. Event cycle mechanism

You don't know when or in what order user clicks, swipes, or hard disk IO accesses occur, so you have to have a never-ending and non blocking loop to deal with these "emergencies" Therefore, the single thread model based on event loop mechanism appears:

The Dart event loop mechanism consists of one message loop and two message queues: event queue and microtask queue

Event Looper

After Dart executes the main function, the Event Looper starts working. The Event Looper takes priority to complete all the events in the Microtask Queue. The event in the Event Looper will not be executed until the Microtask Queue is empty. When the Event Looper is empty, the loop can be exited

Note: when Event Looper is empty, you can exit, but not necessarily, depending on the scene

Event Queue

The event of Event Queue comes from external events and Future

  • External events: such as input / output, gesture, drawing, timer, Stream, etc
  • Future: used to customize Event Queue events

For external events, once there is no microtask to execute, Event Looper will consider listing as the first item in the queue and execute it

Add an event to the Event Queue through the Future instance:

Future(() {
  // Event task
});

Microtask Queue

  • Microtask Queue takes precedence over Event Queue
  • Usage scenario: you want to complete some tasks later, but you want to execute them before the next event

Microtask is generally used for very short internal asynchronous actions, and the number of tasks is very small. If there are too many microtasks, the Event Queue will not be in line, and the execution of the Event Queue will be blocked (for example, the user clicks and does not respond) Therefore, Event Queue is preferred in most cases, and the whole fluent source code only references the scheduleMicroTask() method seven times

Add a task to the Microtask Queue through the scheduleMicroTask() function:

scheduleMicrotask(() {
  // Micro task
});

2, Future

Asynchronous operations in Dart mainly use future and async/await, which are similar to promise and async/await in front-end ES6. Future can be understood as a class with callback effect

1. Basic use

By looking at the constructor of Future, you know that a function with a return value type of futureor < T > needs to be passed in during creation:

factory Future(FutureOr<T> computation()) {
  ...
}

This futureor < T > is a union type, and the final type may be Future or a specific type of generic t When generic t is not specified, the instance type is Future < dynamic > The following is an example of simulating network time-consuming requests:

Future<String> getNetworkData() {
  // 1. Wrap the time-consuming operation into the callback function of Future
  return Future<String>(() {
    sleep(Duration(seconds: 2));

    return "Hello lqr"; // As long as there is a return result, the callback of then corresponding to Future will be executed (equivalent to promise resolve)
    // throw Exception("error"); //  If no result is returned (with error message), you need to throw an exception in the Future callback (equivalent to promise reject)
  });
}

There are three common methods for Future instances:

  • then((value) {...}): executed during normal operation
  • Catchrror ((ERR) {...}): execute when an error occurs
  • whenComplete(() {...}): it will be executed whether it is successful or not

The execution status and results of the Future instance can be obtained through the above three methods:

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

  // 2. Get the results
  var future = getNetworkData();
  future.then((value) => print(value)) // Hello lqr
      .catchError((err) => print(err))
      .whenComplete(() => print("Execution complete")); // It will be executed whether it is successful or not

  print("main end");
}

The log output is as follows:

main start
main end

// Output after 2 seconds:
Hello lqr
 Execution complete

Note that the above three methods can be written separately, but each time a method is executed, the future instance needs to be re assigned (equivalent to a layer of package), otherwise the subsequent methods are invalid:

var future = getNetworkData();

// Incorrect writing:
future.then((value) => print(value));
future.catchError((error) => print(error)); // invalid

// Correct writing:
future = future.then((value) {
  print(value);
  return value;
});
future.catchError((error) => print(error)); // Effective

2. Chain call

Future can return another future instance in the then() method to achieve the effect of chain call, which is very useful for those network requests with data association:

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

  // Chain call to execute multiple data processing
  Future(() {
    return "First result";
  }).then((value) {
    print(value);
    return "Second result";
  }).then((value) {
    print(value);
    return "Third result";
  }).then((value) {
    print(value);
  }).catchError((error) {
    print(error);
  });

  print("main end");
}

Emphasis: the Future constructor requires that a function whose return value type is futureor < T > be passed in, but because futureor < T > is a union type, another Future instance or a specific type of data, such as string, can be returned here

3. Other API s

In addition to the default constructor, Future also provides several common named constructors:

  • Future.value(): create a future instance that returns specific data
  • Future.error(): create a future instance that returns an error
  • Future.delayed(): create a delayed future instance
main(List<String> args) {
  print("main start");

  Future.value("Hello lqr").then((value) => print(value));

  Future.error("Error ").catchError((error) => print(error));

  Future.delayed(Duration(seconds: 3))
      .then((value) => "Hello lqr")
      .then((value) => print(value));

  Future.delayed(Duration(seconds: 2), () => "Hello lqr")
      .then((value) => print("welcome"))
      .then((value) => throw Exception("Error "))
      .catchError((error) => print(error))
      .whenComplete(() => print("Execution complete")); // It will be executed whether it is successful or not

  print("main end");
}

3, async/await

async/await is a syntax sugar provided by Dart that can realize asynchronous calling process in synchronous code format

1. Basic use

  • await must be used in async functions
  • The result returned by async function must be a Future
Future getNetworkData() async {
  var userId = await getUserId();
  var userInfo = await getUserInfo(userId);
  return userInfo.username // It will be automatically wrapped into Future
}

If async/await is not used, the above code needs to be written as follows:

Future getNetworkData() {
  return getUserId().then((userId) {
    return getUserInfo(userId);
  }).then((userInfo) {
    return userInfo.username;
  });
}

In contrast, the code written in async/await will be clearer in understanding

4, isolate

All Dart codes are run in isolate, which is a small space on the machine with its own private memory block and a single thread running Event Looper Each isolate is isolated from each other and does not share memory like threads Generally, a Dart application can only run all codes in one isolate, but if there are special needs, you can open multiple:

Note: Dart does not have the concept of thread, only isolate

1. Create isolate (Dart API)

Dart provides isolate.com by default Spawn (entryPoint, message) is used to start isolate. You can know from the source code that the formal parameter message is actually the parameter required for the execution of the function corresponding to the formal parameter entryPoint:

external static Future<Isolate> spawn<T>(
    void entryPoint(T message), T message,
    {bool paused = false,
    bool errorsAreFatal = true,
    SendPort? onExit,
    SendPort? onError,
    @Since("2.3") String? debugName});

Use isolate Spawn (entrypoint, message) starts isolate and specifies the task to be executed:

import 'dart:isolate';

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

  Isolate.spawn(calc, 100);

  print("main end");
}

void calc(int count) {
  var total = 0;
  for (var i = 0; i < count; i++) {
    total += i;
  }
  print(total);
}

2. isolate communication (unidirectional)

The only way isolates can work together is by passing messages back and forth Generally, the child isolate will send the running result to the main isolate in the form of message through the pipeline and process the message in the Event Looper of the main isolate. At this time, you need to use ReceivePort to process the message delivery:

  • When the child isolate is started, the send port of the main isolate is passed to the child isolate as a parameter
  • When the child isolate completes execution, it can send information to the main isolate through the pipeline (SendPort)
import 'dart:isolate';

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

  // 1. Create a pipe
  var receivePort = ReceivePort();

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

  // 3. Monitoring pipeline
  receivePort.listen((message) {
    print(message);
    // Close the pipe when it is no longer in use
    receivePort.close();
    // Kill isolate when no longer in use
    isolate.kill();
  });

  print("main end");
}

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

The above only realizes the one-way communication of isolate. The two-way communication is troublesome. If you are interested, you can check some other materials

3. Create isolate (fluent API)

Fluent provides a more convenient API for opening isolate: compute() function The following is sample code:

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

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

compute() is the API of fluent, not Dart. Therefore, the above code can only be run in the fluent project

reference material

Topics: Flutter dart async future