The Dart single-threaded

Dart event loops are single-threaded, providing a smooth and secure experience. The core is divided into main thread, microtask, and macro task. The main thread mainly includes service processing, network I/O, local file I/O, and asynchronous events. Dart has two event queues in a single thread, a microtask queue and an event queue.

  • Microtask queue

Microtask queues contain microtasks within Dart and are scheduled using scheduleMicrotask.

  • The event queue

Event queues contain external events, such as I/O, Timer, draw events, and so on.

Event loop

The chain specifies the order of events

If your code has dependencies, it is best to make them explicit. This helps developers understand your code and makes it more robust.

Error adding a task to an event queue:

future.then(... set an important variable...) ; Timer.run(() {... use the important variable... });Copy the code

True:

future.then(... set an important variable...) .then((_) {... use the important variable... });Copy the code

In the Dart single thread, their results are consistent, but not as easy to understand.

How to Create a Task

When you want to execute your code later in a task, you can use the Future class, which adds tasks to the end of the event queue, or the top-level scheduleMicrotask function, which adds tasks to the end of the microtask queue.

WhenComplate () instead of then for better use, or if you need to execute it when an error occurs.

How do I add tasks to the event queue

You can use either Timer or Future

You can also use Timer to schedule tasks, but if any uncaught exceptions occur in the task, the application will exit. Instead, we recommend using a Future, which builds on a Timer and adds features such as detecting task completion and responding to errors.

Add code to the event queue

new Future(() {
  / /... code goes here...
});
Copy the code

You can use then or whenComplate to perform later tasks. Look at this example

new Future(() => 21)
    .then((v) => v*2)
    .then((v) => print(v));
Copy the code

If you want to do this later, use future.delayed ():

new Future.delayed(const Duration(seconds:1), () {
  / /... code goes here...
});
Copy the code

Now that you know the basics, how do you put them together?

Use the task

With single threads and queues, there must be loops so that different queue tasks can be executed and events processed, so let’s look at loops.

  1. Enter themainFunction and generate the corresponding microtask and event queue
  2. Determine whether there are microtasks. If there are, they will be executed. If there are not, they will continue. After the execution, determine whether there are more microtasks. If there are, perform them. If not, continue
  3. If there is no executable microtask, the system determines whether there is an event task. If there is an event task, the system executes it. If there is no event task, the system returns to determine whether there is an event task
  4. New microtasks and event tasks can also be generated in microtasks and event tasks, so it is necessary to judge whether there are new microtasks and event tasks again.

To verify this, let’s look at the following code:

void main() {
  test();
}

/ / / the task
/ / / timer
void test() async {
  print('start');
  scheduleMicrotask(() {
    print('Microtask 1');
  });
  Future.delayed(Duration(seconds: 0)).then((value) {
    print('Future 1');
  });
  Timer.run(() {
    print('Timer 1');
  });
print('end');
}
Copy the code

The operation process is as follows:

  1. First startmainFunction, printstart
  2. performscheduleMicrotaskMicrotasks, adding tasks to the microtask queue
  3. performFutureEvent to add a task to the event queue
  4. performtimerEvent to add a task to the event queue
  5. Perform event printingend
  6. The second loop checks whether there are microtasks. The microtasks have been added, and now the microtasks are executed, printingMicrotask 1
  7. Check whether there are event tasks, which were added just nowFutureTask to perform printingFuture 1
  8. Check whether there are event tasks, which were added just nowTimer 1Task to perform printingTimer1

The output

start
end
Microtask 1
Future 1
Timer 1
Copy the code

Look at the following example to prove that Timer and Future are one type of event.

/ / / the task
/ / / timer
void test2() async {
  print('start');
  scheduleMicrotask(() {
    print('Microtask 1');
  });
   Timer.run(() {
    print('Timer 1');
  });
  Future.delayed(Duration(seconds: 0)).then((value) {
    print('Future 1');
  });
 

  print('end');
}
Copy the code

The output

start
end
Microtask 1
Timer 1
Future 1
Copy the code

In test and test2 above, the Timer and Future positions are reversed, which means that the task of adding events is reversed in order and executed in reverse order. Let’s look at Future source code:

factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
    _Future<T> result = new _Future<T>();
    new Timer(duration, () {
      if (computation == null) {
        result._complete(null);
      } else {
        try {
          result._complete(computation());
        } catch(e, s) { _completeWithErrorCallback(result, e, s); }}});return result;
  }
Copy the code

The essence of the future.delayed source code is to add a task to a Timer and run the task after a specified time.

After executing the event, you need to determine whether there are microtasks again

The priority of the microtask queue is higher than that of the event queue. Therefore, every time a task is executed, it first checks whether there are unexecuted microtasks.

After the event queue is executed, new microtasks may be added to the queue, so the microtask queue needs to be scanned again. Look at the following example:

void test3() async {
  print('start'); / / 1
  scheduleMicrotask(() {/ / 2
    print('Microtask 1');/ / 5
  });
   Timer.run(() {/ / 3
    print('Timer 1');/ / 6
    Timer.run(() {/ / 9
    print('Timer 1 Microtask 2 ');/ / 10
    });
    scheduleMicrotask(() {/ / 7
        print('Microtask 2');/ / 8
     });
  });
  print('end');/ / 4
}
Copy the code

Order of execution:

  1. printstart
  2. Add a microtask to the queue
  3. addTimer 1To the event queue
  4. Perform printendtask
  5. Determine if there are microtasks and find microtasksMicrotask 1, execute immediately
  6. Determine whether there are microtasks, and find whether there are event tasks, and findTimer 1Task and execute, add event taskTimer 1 Microtask 2, add microtasksMicrotask 2To the microtask queue
  7. Determine if there are microtasks and find microtasksMicrotask 2And execute.
  8. Check whether event tasks exist and discover event tasksTimer 1 Microtask 2And executed.

Results output

start
end
Microtask 1
Timer 1
Microtask 2
Timer 1 Microtask 2
Copy the code

Will microtasks or event tasks get stuck?

According to the previous understanding, the event task is executed after the micro-task is executed. When an event takes a long time to process, the subsequent tasks will always be in the waiting state. When there are enough tasks, it still takes some time to complete them.

void test4() async {
  
  print('start ${DateTime.now()}');
  for(int i =0; i < 99999; i++){ scheduleMicrotask(() {print('Microtask 1');
    });
  }
   Timer.run(() {
    print('Timer 1 ${DateTime.now()}');  
  });
  print('end ${DateTime.now()}');
}
Copy the code

Output:

Start 2020-07-28 17:44:11.561886 end 2020-07-28 17:44:11.593989... Microtask 1 ..... The Timer 1 2020-07-28 17:44:11. 893093Copy the code

It can be seen that the execution time of these tasks basically reached 0.33 seconds.

When a thread is running out of processing tasks, another thread needs to be started.

Isolates multithreading

In DART, threads are called turbo threads. Each thread does not share memory and communicates through a messaging mechanism.

Let’s look at an example of using Dart to achieve multithreading.

void test5()async{
  final rece = ReceivePort();
  isolate = await Isolate.spawn(sendPort, rece.sendPort);
   rece.listen((data){
     print(${data},name:$name);
   });
}
void sendPort(SendPort sendPort){
  sendPort.send('Send message');
}

Isolate isolate;
String name='fgyong';
void main() {
  test(5); }Copy the code

The output

The send message name: fGYong was receivedCopy the code

How to deal with multi-thread communication?

After the thread is created, the child thread needs to send a port and a message to the main thread. The main thread records this port and uses this port to communicate with the child thread next time.

The specific code is as follows:


/// the new thread executes a new task and listens
Isolate isolate;
Isolate isolate2;

void createTask() async {
  ReceivePort receivePort = ReceivePort();
  isolate = await Isolate.spawn(sendP1, receivePort.sendPort);
  receivePort.listen((data) {
    print(data);
    if (data is List) {
      SendPort subSencPort = (data as List) [1];
      String msg = (data as List) [0];
      print('$MSG received on main thread ');
      if (msg == 'close') {
        receivePort.close();
      } else if (msg == 'task') {
        taskMain();
      }
      subSencPort.send(['Main thread issued']); }}); }void sendP1(SendPort sendPort) async {
  ReceivePort receivePort = new ReceivePort();
  receivePort.listen((data) async {
    print(data);
    if (data is List) {
      String msg = (data as List) [0];
      print('$MSG received in child thread ');
      if (msg == 'close') {
        receivePort.close();
      } else if (msg == 'task') {
        var m = await task();
        sendPort.send(['$m', receivePort.sendPort]); }}}); sendPort.send(['Child thread thread emitted', receivePort.sendPort]);
}

Future<String> task() async {
  print('Child thread executes task');
  for (var i = 0; i < 99999999; i++) {}
  return 'task to complete;
}

void taskMain() {
  print('Main thread executes task');
}
Copy the code

Output:

SendPort sent by the child thread received on the main thread. Sent by the main thread Received on the child threadCopy the code

See the code base for more child threads interacting with the main thread

Complex problem solutions

Imagine a project that requires two teams to complete and involves multiple tasks. It can be divided into two high-priority tasks (a high-priority task generates two tasks, one urgent and one non-urgent) and two non-high-priority tasks (a non-high-priority task generates two tasks, one urgent and one non-urgent). There is also one that you have to rely on other teams to do because the team doesn’t have the resources to do that, and a third party can also produce a high-priority task and a low-priority task.

According to the urgent tasks as micro tasks, non-urgent tasks as event tasks are arranged, and the third party is the newly opened thread

The main task High priority (microtasks) Low priority (event task) A third party (Isolate)
H1 H1-1 L1-2 no
H2 H2-1 L2-2 no
L3 H3-1 L3-2 no
L4 H4-1 L4-2 no
I5 IH5-1 I5-2 is
void test6() {
  createTask();// Create a thread
  scheduleMicrotask(() {// The first microtask
    print('H1');
    scheduleMicrotask(() {// First urgent task
      print('H1-1');
    });
    Timer.run(() {// First non-urgent task
      print('L1-2');
    });
  });
  scheduleMicrotask(() {// The second high-priority task
    print('H2');
    scheduleMicrotask(() {// Second urgent task
      print('H2-1');
    });
    Timer.run(() {// Second non-urgent task
      print('L2-2');
    });
  });
  Timer.run(() {// The first low-priority task
    print('L3');
    scheduleMicrotask(() {// Third urgent task
      print('H3-1');
    });
    Timer.run(() {// Third non-urgent mission
      print('L3-2');
    });
  });

  Timer.run(() {// Second low priority task
    print('L4');
    scheduleMicrotask(() {// Urgent task number four
      print('H4-1');
    });
    Timer.run(() {// The fourth non-urgent task
      print('L4-2');
    });
  });
}

/// the new thread executes a new task and listens
Isolate isolate;
void createTask() async {
  ReceivePort receivePort = ReceivePort();
  isolate = await Isolate.spawn(sendPort, receivePort.sendPort);
  receivePort.listen((data) {
    print(data);
  });
}
/// The new thread executes the task
void sendPort(SendPort sendPort) {

  scheduleMicrotask(() {
    print('IH5-1');
  });
  Timer.run(() {
    print('IL5-2');
  });
  sendPort.send('The third party has completed the task');
}
Copy the code

The results

H1 H2 H1-1 H2-1 L3 H3-1 L4 H4-1 L1-2 L2-2 L3-2 L4-2 IH5-1 IL5-2 The third-party task is completedCopy the code

It can be seen that the items starting with H are of high priority and those starting with L are of low priority. The basic high-priority items are run before the low-priority items, as expected.

But why was the third party implemented at the end?

Since the creation of the thread requires events, and other tasks are too time-consuming, we can do another task with long events.

createTask(); // Create a threadfor (var i = 0; i < 9999999999; i++) {
    
  }
  ...
Copy the code

Output:

Ih5-1 IL5-2 H1 H2 H1-1 H2-1 Third-party task End L3 H3-1 L4 H4-1 L1-2 L2-2 L2-2 L3-2 L4-2Copy the code

whyThe third-party task is completeRight in the middle? Not above H1? ?

Because this event is a low priority, whereas H is a high priority task.

whyThe third-party task is complete ??

It’s above L3. It’s not at the bottom, it has to be at the top of the lower priority queue?

The duration of the start operation event is too long. As a result, the third-party task has been executed before all tasks are executed. Therefore, the third-party task is added to the low-priority task queue first after the completion of the task.

If the number of time-consuming operations is small, the sequence of adding tasks is uncertain.

conclusion

Dart separates asynchrony from multithreading, where asynchrony is simply the result of multi-event training in an event loop, whereas multithreading can be understood as true concurrency (multiple threads working at the same time). In a single thread, it is divided into microtasks and other event queues, and the priority of microtasks queue is higher than other event queues.

reference

  • Flutter is quick to use and learn
  • The Event Loop and Dart
  • Code base view demo