In-depth study of Dart threading model

【Abstract】 Both Java and Objective-C are programming languages ​​with a multi-threaded model. When any thread triggers an exception and the exception is not caught, the entire process will exit. But Dart and JavaScript will not, they are both single-threaded models, and the operating mechanism is very similar (but there are differences).

  This article is shared from Huawei Cloud Community " Deep Learning Dart Threading Model ", author:  IT programming technology learning stack

Dart single-threaded model

Both Java and OC are programming languages ​​​​with a multi-threaded model. When any thread triggers an exception and the exception is not caught, it will cause the entire process to exit. But Dart and JavaScript will not, they are both single-threaded models, and the operating mechanism is very similar (but there are differences).

Dart runs with a message loop mechanism in a single thread, which contains two task queues, one is the "microtask queue"  microtask queue , and the other is called the "event queue"  event queue . The execution priority of the microtask queue is higher than that of the event queue.

The general operating principle of Dart: first open the app to execute the entry function main(). After the execution is completed, the message mechanism starts. First, the tasks microtask in the microtask queue will be executed one by one in the order of first-in-first-out, and the event task eventtask will exit after execution . , However, new microtasks and event tasks can also be inserted during the execution of event tasks. In this case, the execution process of the entire thread is always looping and will not exit. In Flutter, the execution of the main thread The process is just that, never ending.

In the event loop, when an exception occurs in a task and is not caught, the program will not exit, and the direct result is that the subsequent code of the current task will not be executed, that is to say, the exception in a task is It will not affect the execution of other tasks.

The execution sequence of events in Dart: Main > MicroTask > EventQueue

Usually use the scheduleMicrotask(…) or Future.microtask(…) method to insert a task into the microtask queue.

Usually use Future to add events to EventQueue, or use async and await to add events to EventQueue.

For example:

void main(){
  run();
}

void run(){

  Future(() async {

    print("EvenTask1");

  });

  evenTest();

  Future(() async{

    print("EvenTask2");

    await Future((){

      print("EvenTask4");

    });

    print("EvenTask3");

  });

  Future.microtask(() => print("MicroTask1"));

  scheduleMicrotask((){

    print("MicroTask2");

  });

  print("main");

}

void evenTest() async{

  await Future((){

    print("EvenTask5");

  });

}

The output is:

main
MicroTask1
MicroTask2
EvenTask1
EvenTask5
EvenTask2
EvenTask4
EvenTask3

In Dart, all external event tasks are in the event queue, such as IO, timers, clicks, and drawing events, etc., and microtasks usually come from inside Dart, and there are very few microtasks. The reason for this is because microtasks The priority of the task queue is high. If there are too many microtasks, the total execution time will be longer, and the delay of the event queue tasks will be longer. long.

In-depth understanding of Flutter and Dart single-threaded models

As we all know, Java is a multi-threaded language. Appropriate and appropriate use of multi-threading will greatly improve resource utilization and operating efficiency, but the disadvantages are also obvious. For example, opening too many threads will lead to excessive resource and performance consumption and Multithreaded shared memory is prone to deadlock .

Dart is a single-threaded language. A single-threaded language means that the code execution sequence is orderly. Let's combine a demo to learn more about the single-threaded model.

Demo example:

Clicking a button will call the following method to read a json file about 2M in size.

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
}

After clicking the refresh button, as shown in the figure below, the loading in the middle will be stuck. Many students will know when they look at this code that they will definitely get stuck. When parsing a 2M file, and it is parsed synchronously, the main page will definitely get stuck.

So what if I switch to asynchronous parsing? Still stuck? You can think about this question in your mind.

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  // 异步解析
  Future(() {
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
    VideoListModel.fromJson(json.decode(jsonStr));
  }).then((value) {});
}

As you can see, I have already parsed it in asynchronous, why is it still stuck? You can think about this question first.

As mentioned earlier,  Dart is a single-threaded language, which means that the code execution sequence is in order. Of course, Dart also supports asynchronous. These two points are actually not in conflict.

Dart thread analysis

Let's take a look at Dart's thread. When our main() method starts, Dart has started a thread, and the name of this thread is Isolate. Each Isolate thread contains the two queues shown in the figure, a Microtask queue and an Event queue.

The Isolate thread will execute the events in the Microtask queue first. When the events in the Microtask queue become empty, the events in the Event queue will be executed. If the events in the Microtask queue are being executed, the events in the Event queue will be blocked, resulting in no response to rendering, gesture response, etc. (drawing graphics, processing mouse clicks, processing file IO, etc. are all in the Event Queue completed).

Therefore, in order to ensure the normal use of the function without lagging, try to do as little as possible in the Microtask queue, and you can do it in the Event queue.

Why can a single thread do an asynchronous operation?

Because the APP will only respond to events when you swipe or click. When there is no operation, enter the waiting time, and both queues are empty. This time can be used for asynchronous operations, so based on this feature, the single-threaded model can perform some asynchronous operations during the waiting process, because the waiting process is not blocked, so it feels like doing multiple things at the same time , but only one thread is doing things all the time.

Future

When the async keyword is added to the method, it means that the method starts an asynchronous operation. If the method has a return value, it must return a Future.

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  // 异步解析
  Future(() {
    ...
  }).then((value) {});
}

The execution of a Future asynchronous task is relatively simple. After we declare a Future, Dart will execute the asynchronous code function body in the Event queue and return. Note here that Future and then are placed in the same Event queue.

Suppose, I did not execute the then method immediately after executing the Future code, but waited 5 seconds after the Future was executed before calling the then method. Is it still placed in the same Event queue at this time? Obviously it is impossible , let's take a look at how the source code is implemented.

Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
  ...
  _addListener(new _FutureListener<T, R>.then(result, f, onError));
  return result;
}

bool get _mayAddListener => _state <= (_statePendingComplete | _stateIgnoreError);

void _addListener(_FutureListener listener) {
  assert(listener._nextListener == null);
  if (_mayAddListener) {
    // 待完成
    listener._nextListener = _resultOrListeners;
    _resultOrListeners = listener;
  } else {
    // 已完成
    ...
    _zone.scheduleMicrotask(() {
      _propagateToListeners(this, listener);
    });
  }
}

You can see that  there is a monitor in the then method, which is called only 5 seconds after the Future is executed. It is obviously completed. Go to the scheduleMicrotask() method in the else, that is, put the method in then into the Microtask  queue .

Why does Future lag?

Let’s talk about the question just now. I have already parsed it in asynchronous, why is it still stuck?

In fact, it is very simple. The code in the Future may need to execute for 10s, that is, the Event queue needs 10s to be executed. Then other codes within this 10s will definitely not be able to execute. Therefore, the execution time of the code in the Future is too long, and the UI will still be stuck.

Taking Android as an example, the refresh rate of Android is 60 frames per second, and the Android system will send a VSYNC (synchronization) signal every 16.6ms to trigger the rendering of the UI. So we have to consider, once the code execution time exceeds 16.6ms, should it be executed in Future?

At this time, do some students have questions, my network request is also written in Future, why is it not stuck?

Everyone needs to pay attention to this. The network request is not executed at the Dart level. It is executed by the asynchronous thread provided by the operating system. When the asynchronous execution is completed, the system returns to Dart. So even if the http request takes more than ten seconds, it will not feel stuck.

compute

Since the execution of Future will also freeze, how to optimize it? At this time, we can open a thread operation, and Flutter has encapsulated a compute() method for us, which can open a thread for us. We use this method to optimize the code, and then look at the execution effect.

void loadAssetsJson() async {
  var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

  var result = compute(parse,jsonStr);
}

static VideoListModel parse(String jsonStr){
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  VideoListModel.fromJson(json.decode(jsonStr));
  return VideoListModel.fromJson(json.decode(jsonStr));
}

You can see that when you click the refresh button at this time, it is no longer stuck. This is indeed a better solution for some time-consuming operations.

Let's take a look at how the DefaultAssetBundle.of(context).loadString(“assets/list.json”) method is executed.

Future<String> loadString(String key, { bool cache = true }) async {
  final ByteData data = await load(key);
  if (data == null)
    throw FlutterError('Unable to load asset: $key');
  // 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
  // on a Pixel 4.
  if (data.lengthInBytes < 50 * 1024) {
    return utf8.decode(data.buffer.asUint8List());
  }
  // For strings larger than 50 KB, run the computation in an isolate to
  // avoid causing main thread jank.
  return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}

As can be seen from the official source code, when the file size exceeds 50kb, the compute() method is also used to open a thread to operate.

multithreading mechanism

As a single-threaded language, Dart provides a multi-threaded mechanism, but the resources of the multi-threads are isolated, and the resources between the two threads are not interoperable.

Dart's multi-threaded data interaction needs to be passed from A thread to B thread, and then B thread returns to A thread. And like Android opens a sub-thread in the main thread, the sub-thread can directly get the data of the main thread without letting the main thread pass it to the sub-thread.

Summarize

1. Future is suitable for operations that take less than 16ms

2. Time-consuming operations can be performed through compute()

3.Dart is a single-threaded reason, but it also supports multi-threading, but the data between threads is not interoperable

4. The Isolate thread will execute the events in the Microtask queue first. When the events in the Microtask queue become empty, the events in the Event queue will be executed. If the events in the Microtask queue are being executed, the events in the Event queue will be blocked, resulting in no response to rendering, gesture response, etc. (drawing graphics, processing mouse clicks, processing file IO, etc. are all in the Event Queue completed).

5. The execution sequence of events in Dart: Main > MicroTask > EventQueue

6. The code wrapped with await and Future is run in the eventQueue, and the then of the Future code without delay is also executed in the same eventQueue. Only the delayed code Future and then will be processed separately, and then enters the microTask queue.

Guess you like

Origin blog.csdn.net/qq_48892708/article/details/129845561