Event loops are a common thing in UI frameworks. For example, android main thread has a Looper that reads events in MessageQueue. There’s something similar in Flutter.

In fact, the Flutter event loop should be something that Dart supports at the language level. Dart is a single-threaded programming language, with one thread for each Isolate, and each Isolate has an event loop. It is worth noting that although you can start multiple isolates to achieve multi-threading, as the name suggests, the ISOLates are memory isolated from each other. They have separate heap memory, which means that both isolates do not have thread-safety issues:

Once the Isolate starts, it starts the event loop by default and doesn’t require us to do anything. One of the interesting things about this is that, unlike other languages, the whole program ends and exits after main. Dart’s main function enters an event loop after execution, listens for events and performs callbacks, such as keystroke events and Timer events:

So from this perspective, Dart’s main function is more like an init function, which means the Dart program starts, but the Dart program runs after it exits. For example:

void main() {
  print("main begin");
  Timer(Duration(seconds: 3), () {
    print("on timer");
  });
  print("main begin finish");
}
Copy the code

If the Timer expires after 3 seconds, “on Timer “will still be printed even after main exits:

I/flutter (15551): main begin
I/flutter (15551): main begin finish
I/flutter (15551): on timer
Copy the code

The message queue

Message loops are often used with message queues, such as Looper and MessageQueue in Android. Dart has two message queues, microTask queue and Event queue. The time loop will execute the tasks in the MicroTask queue, and the tasks in the Event queue will be executed after the MicroTask queue is empty:

The event queue contains Dart and events from other parts of the system. But the MicroTask queue only contains the internal code from the current ISOLATE. If we want to do something after this event loop and before the next event loop, we can add tasks to the MicroTask queue:

void main() {
  new Future(() {
    scheduleMicrotask(()=>print("in microtask queue 1"));
    print("in event queue 1");
  });
  new Future(() {
    scheduleMicrotask(()=>print("in microtask queue 2"));
    print("in event queue 2");
  });
  new Future(() {
    scheduleMicrotask(()=>print("in microtask queue 3"));
    print("in event queue 3");
  });
}
Copy the code

In the main function, we used the Future to insert three tasks into the Event queue, but when each task is executed, we also insert microtasks into the MicroTask queue. This causes the microTask queue to be retraversed after the task has executed to find the MicroTask to execute:

in event queue 1
in microtask queue 1
in event queue 2
in microtask queue 2
in event queue 3
in microtask queue 3
Copy the code

A Future has a then method that performs the specified action after the Future executes:

void main() {
  new Future(() {
    scheduleMicrotask(()=>print("in microtask queue 1"));
    print("in event queue 1");
    return "a";
  }).then((a) {
    scheduleMicrotask(()=>print("in microtask queue 2"));
    print("in event queue 2 -> $a");
    return new Future(() { return "b"; }); }).then((b) { scheduleMicrotask(()=>print("in microtask queue 3"));
    print("in event queue 3 -> $b");
  });
}
Copy the code

At first, I mistakenly thought that I was inserting another Future after this Future task. In fact, then contains a callback whose argument is the return value of the previous callback. Callback is executed immediately after the Future completes execution, without inserting the Event queue. But if the previous callback returns a Future, the task is inserted into the Event queue, and the subsequent callback actually listens for the returned Future. So the code above is printed as follows:

I/flutter (23893): in event queue 1
I/flutter (23893): in event queue 2 -> a
I/flutter (23893): in microtask queue 1
I/flutter (23893): in microtask queue 2
I/flutter (23893): in event queue 3 -> b
I/flutter (23893): in microtask queue 3
Copy the code

Single threaded model

Thread block

Dart is a single-threaded model, and since the event loop is within a single thread, it blocks subsequent tasks if our task takes a long time:

void main() {
  print('start time :' + DateTime.now().toString()); // The current time
  Timer(Duration(seconds: 1), () {
    //callback function
    print('first timer :' + DateTime.now().toString()); / / 1 s
    sleep(Duration(seconds: 3));
  });
  Timer(Duration(seconds: 2), () {
    //callback function
    print('second timer :' + DateTime.now().toString()); / / 2 s after
  });
}
Copy the code

So our second Timer is set to execute after 2 seconds, but in fact it will be blocked by the first Timer’s 3-second sleep until the end of the first Timer:

I/flutter (21196): start time: 21-10-20 21:30:11.794680 I/flutter (21196): start time: 21-10-20 21:30:11.794680 I/ FLUTTER (21196): Second timer :2021-10-20 21:30:15.841398Copy the code

So we shouldn’t put too much trust in these timed tasks.

An exception that is not caught

Unlike Java, Kotlin and other multithreaded languages, if an uncaught exception occurs in a thread, the thread is forced to terminate. Dart does not require us to handle exceptions because of the event loop mechanism used to run relatively independent tasks. If a task is abnormal, it terminates the task but does not affect the entire thread. Subsequent tasks can continue to execute:

void main() {
  new Future(() {
    print("task1 begin");
    throw new Exception();
    print("task1 finish");
  });
  new Future(() {
    print("task2 begin");
    print("task2 finish");
  });
}
Copy the code

Task1 is interrupted, but Task2 will still execute:

task1 begin
Error: Exception
    at Object.throw_ [as throw] (http://localhost:64924/dart_sdk.js:5041:11)
    at http://localhost:64924/packages/myflutter/main.dart.lib.js:365:17
    at http://localhost:64924/dart_sdk.js:32040:31
    at internalCallback (http://localhost:64924/dart_sdk.js:24253:11)
task2 begin
task2 finish
Copy the code

Implement multithreading in Dart

While it is possible to implement functionality similar to Kotlin’s coroutines with Dart’s async await, these coroutines actually run in the same thread unless special operations are done. Once a coroutine performs time-consuming operations such as complex calculations, the UI will stall. At this point we can create multiple isolates to implement something similar to multithreading:

int globalData = 1;

void otherIsolate(SendPort sendPort) {
  while(true){
    globalData ++;
    sleep(Duration(seconds: 1));
    sendPort.send("globalData from otherIsolate $globalData"); }}void main() {
  globalData = 100;

  ReceivePort receivePort = ReceivePort();
  receivePort.listen((message) {
    print(message);
  });

  Isolate.spawn(otherIsolate, receivePort.sendPort);
  Future.delayed(Duration(seconds: 3), () = > {print("globalData in Future : $globalData")}); }Copy the code

We can communicate with each other between Isolate through ReceivePort, something like a conduit. As mentioned earlier, Isolate is memory isolated, which is more of a process concept. Therefore, it is not possible to exchange data through global variables like other languages’ multithreading:

I/flutter (17503): globalData from otherIsolate 2
I/flutter (17503): globalData from otherIsolate 3
I/flutter (17503): globalData in Future : 100
I/flutter (17503): globalData from otherIsolate 4
I/flutter (17503): globalData from otherIsolate 5
I/flutter (17503): globalData from otherIsolate 6
Copy the code