Dart Asynchronous principle

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

Asynchronous IO + event loop

1. I/O model

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

String text = io.read(buffer); // block waitCopy 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 just sits there waiting for the result to come back, doing nothing. Blocking IO, right

Two concepts are common here: blocking and non-blocking invocation

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.

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); 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 one IO returns data, return immediately; Status = select(io_array, timeout); If (status == OK){for(IO in io_array){io.read()}}Copy the code

With IO multiplexing, CPU utilization efficiency is improved.

In the above code, the thread could 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

Dart doesn’t stall for single-thread I/O, but how does the main thread deal with a large number of asynchronous messages? Let’s move on to Dart’s Event Loop.

2. Event Loop

The full version of the Event Loop flowchart

The Dart event loop mechanism consists of an Event looper and two message queues, the Event queue and the Microtask queue. The operating principle of this mechanism is as follows:

  • First, the Dart program starts from main. After main completes, the Event Looper starts.
  • The Event Looper then takes precedence over all events that execute the Microtask queue until the Microtask queue is empty.
  • The Event Looper then iterates through all events in the execution Event queue until the event queue is empty.
  • Finally, exit the loop as appropriate.

Micro tasks

A microtask, as its name suggests, is an asynchronous task that is completed in a short period of time. As you can see from the above flow chart, the microtask queue has the highest priority in the event cycle and can occupy the event cycle as long as there are tasks in the queue.

Microtasks are created by scheduleMicroTask

scheduleMicrotask(() => print('This is a microtask'));
Copy the code

However, asynchronous tasks rarely have to be performed in front of an event queue and therefore do not require high priority. Therefore, microtask queues are rarely used directly, even inside Flutter (for example, Gesture recognition, text entry, scrolling view, saving page effects, etc.).

Event Queue

Dart provides a layer of encapsulation for the task creation of the Event Queue, called the Future. It is also easy to understand from the name that it represents a task to be completed at a future time.

3, Isolate

Although Dart is based on a single-threaded model, to further leverage multi-core cpus and Isolate CPU-intensive operations, Dart also provides a multi-threaded mechanism, known as Isolate. Each Isolate has its own Event Loop and Queue. The Isolate does not share any resources and can only communicate with each other through the message mechanism. Therefore, resource preemption is not a problem.

If the Isolate needs to communicate with each other (one-way or bidirectional), they can only write tasks to each other’s event loop queue. The communication between them is realized through port, which is divided into receivePort and sendPort. They come in pairs. Communication between Isolate:

  • First, the current Isolate creates a ReceivePort object and gets the corresponding SendPort object.
 var receivePort = ReceivePort();
 var sendPort = receivePort.sendPort;
Copy the code
  • Second, a new Isolate was created and asynchronous tasks were performed by the new Isolate. At the same time, the SendPort object of the current Isolate was passed to the new Isolate so that the new Isolate could use the SendPort object to send events to the original Isolate.
// Call isolate. spawn to create a new ISOLate. spawn Var anotherIsolate = await isolate. spawn(otherIsolateInit, receivePort.sendport); // Asynchronous task to be performed by the new Isolate // Call sendPort of the current Isolate to send a message to its receivePort void otherIsolateInit(sendPort sendPort) Async {value = "Other Thread!" ; sendPort.send("BB"); }Copy the code
  • Then, the listen method of the current Isolate#receivePort is called to listen for data from the new Isolate. Can all data types be transferred between Isolate without any markup
ReceivePort. Listen ((date) {print("Isolate 1 accept message: data = $date"); });Copy the code
  • Finally, the message is sent and the newly created Isolate is shut down.
anotherIsolate? .kill(priority: Isolate.immediate); anotherIsolate =null;Copy the code

Future asynchronous details

The introduction of the Future

In the process of writing a program, there is bound to be some time-consuming code that needs to be executed asynchronously. For network operations, we need to request data asynchronously, and we need to handle both successful and failed requests.

In Flutter, a Future is used to perform time-consuming operations indicating that a value will be returned in the Future, and a callback can be registered to listen for Future processing results using the THEN () method and catchError ().

Future<Response> respFuture = http.get('https://example.com'); Then ((response) {if (response.statusCode == 200) {var data = reponse.data; }}). CatchError ((error) {// fail (error); });Copy the code

This pattern simplifies and unifies asynchronous processing

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.

A Future object can have two states

  • Pending: Indicates that the Future object is still being evaluated and no result is available.
  • Completed: Indicates that the Future object has been evaluated. There are two possible outcomes: a correct outcome and a failed outcome.

A constructor

I looked at the API and saw that the Future had six constructors in total

  • 1. Default constructorFuture(FutureOr<T> computation())
  • 2,Future.micortaskA constructor
  • 3,Future.sync(FutureOr<T> computation())A constructor
  • 4,Future.value([FutureOr<T>? value])A constructor
  • 5,Future.error(Object error, [StackTrace? stackTrace])A constructor
  • 6,Future.delayed(Duration duration, [FutureOr<T> computation()?])A constructor

1. Default constructor

A Future object can be created using the default constructor for the Future. The default constructor has the following signature

factory Future(FutureOr<T> computation()) { _Future<T> result = new _Future<T>(); Timer.run(() { try { result._complete(computation()); } catch (e, s) { _completeWithErrorCallback(result, e, s); }}); return result; }Copy the code

The parameter type is FutureOr

Computation (), which means functions that return values of type FutureOr

.

Future, Computation functions created through this method are added to the Event queue for execution.

2,Future.micortaskA constructor

factory Future.microtask(FutureOr<T> computation()) { _Future<T> result = new _Future<T>(); scheduleMicrotask(() { try { result._complete(computation()); } catch (e, s) { _completeWithErrorCallback(result, e, s); }}); return result; }Copy the code

The Computation function is added to the MicroTask queue through the scheduleMicrotask method, taking precedence over the Event queue.

    Future(() {
      print("default fauture");
    });

    Future.microtask(() {
      print("microtask future");
    });
Copy the code

For example, the above method prints the MicroTask Future first

3,Future.syncA constructor

factory Future.sync(FutureOr<T> computation()) {}
Copy the code

Computation will be performed on the current task rather than adding computation to the task queue.

4,Future.value()A constructor

Creates a Future that returns the specified value

factory Future.value([FutureOr<T>? value]) {
    return new _Future<T>.immediate(value == null ? value as T : value);
Copy the code
var future = Future.value(1);
var future1 = Future.value('1');
print(future);
print(future1);
Copy the code

5,Future.errorA constructor

factory Future.error(Object error, [StackTrace? stackTrace]) {}
Copy the code

Creating a Future with an error object and an optional stackTrace can be used to create a Future object with a failed state.

6,The Future of ()A constructor

factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) { if (computation == null && ! typeAcceptsNull<T>()) { throw ArgumentError.value( null, "computation", "The type parameter is not nullable"); } _Future<T> result = new _Future<T>(); new Timer(duration, () { if (computation == null) { result._complete(null as T); } else { try { result._complete(computation()); } catch (e, s) { _completeWithErrorCallback(result, e, s); }}}); return result; }Copy the code

Create a future that is deferred. For example, in the following example, a string can be printed after a Future delay of two seconds.

var futureDelayed = Future.delayed(Duration(seconds: 2), () {
  print("Future.delayed");
  return 2;
});
Copy the code

A static method

1, wait

static Future<List<T>> wait<T>(Iterable<Future<T>> futures, {bool eagerError = false, void cleanUp(T successValue)? {}})Copy the code

The wait static method can wait for multiple futures to complete and fetch the results of all of them via a List. If an exception occurs in one of the Future objects, the final result will be failed

Optional parameters

  • 1.eagerError:eagerError defaults to false. When an exception occurs in a Future, the Future will not be in the failed state immediately, but will be changed to the failed state after all Future results are available. If set to true, when an exception occurs in one of the futures, the final result will immediately be failed
  • 2,cleanUp: If the cleanUp argument is set, when an exception occurs in one of multiple futures, the (non-null) results of other successful futures are passed to the cleanUp argument. The cleanUp function will not be called if no exception occurs.

2, forEach

static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {}
Copy the code

ForEach static methods can perform one operation over each element in Iterable. If the iteration returns a Future object, the next iteration is performed after the Future is complete, and null is returned when the Future is complete. If an exception occurs during an operation, the traversal stops and the final Future is in failed state.

For example, in the following example, {1,2,3} creates three futures with a delay corresponding to the number of seconds. The result is that 1 is printed after 1 second, 2 seconds after 2 seconds, and 3 seconds after 3 seconds. The total time is 6 seconds.

Future.foreach ({1,2,3}, (num){return future.delayed (Duration(seconds: num),(){print(num); }); });Copy the code

3, any

static Future<T> any<T>(Iterable<Future<T>> futures) {}
Copy the code

Returns the result of the first future executed, regardless of whether the result is correct or error

4, doWhile

Repeat an action until false or Future is returned to exit the loop

static Future doWhile(FutureOr<bool> action()) {}
Copy the code

Usage scenario: Suitable for some scenarios that require recursive operations.

For example, in the following example, generate a random number and wait until the operation ends after 10 seconds.

void futureDoWhile(){ var random = new Random(); var totalDelay = 0; Future .doWhile(() { if (totalDelay > 10) { print('total delay: $totalDelay seconds'); return false; } var delay = random.nextInt(5) + 1; totalDelay += delay; return new Future.delayed(new Duration(seconds: delay), () { print('waited $delay seconds'); return true; }); }) .then(print) .catchError(print); } // Output result: I/flutter (11113): waited 5 seconds I/flutter (11113): waited 1 seconds I/flutter (11113): waited 3 seconds I/flutter (11113): waited 2 seconds I/flutter (11113): total delay: 12 seconds I/flutter (11113): nullCopy the code

The processing results

1, then

After you have created a Future object, you can receive the results of the Future through the then method.

Future<R> then<R>(FutureOr<R> onValue(T value), {Function onError});
Copy the code
Future<Response> respFuture = http.get('https://example.com'); Then ((response) {if (response.statusCode == 200) {var data = reponse.data; }}). CatchError ((error) {// fail (error); });Copy the code

2, catchError

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

3, whenComplete

Future.whenComplete is always called after the Future is complete, regardless of whether the result of the Future is correct or wrong.

Future<T> whenComplete(FutureOr<void> action());
Copy the code

4. Timeout method

Future<T> timeout(Duration timeLimit, {FutureOr<T> onTimeout()})
Copy the code

The timeout method creates a new Future object that takes a timeLimit parameter of type Duration to set the timeout. If the original Future completes before the timeout, the final result is the value of the original Future; If the timeout has not been completed, a TimeoutException is raised. This method has an onTimeout optional argument, which, if set, will be called when a timeout occurs and will return a new value for the Future without a TimeoutException.

Async and await

Imagine a scenario like this:

    1. First call the login interface;
    1. Obtain user information based on the token returned by the login interface.
    1. 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){// Obtain 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 in async function body 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