Flutter 非同步機制:microtask

無若葉發表於2021-07-10

之前說到 Flutter 中與非同步相關的 Future,瞭解到,dart 是一個單執行緒模型的語言,當我們使用 Future 去實現一個非同步操作時,多半是利用單執行緒的事件迴圈機制來模擬,類似於 Android 中的 Looper。

不過 dart 中的事件迴圈其實還是分兩種的,也就是網上常說的兩個迴圈佇列,microtask queue 和 event queue,在介紹 Future 的那篇文章裡面其實主要說到的是 event queue 這種模式的運作方式,所以打算在此再詳細說下 microtask queue 的一些實現細節,以此來說明,為何 microtask queue 一定會優先於 event queue 執行。

就拿 Future 來說,它就可以支援跑在兩種佇列上,當我們直接呼叫 Future()Future.delayed() 的時候,內部都是通過 Timer 實現的,其內部就會利用 ReceivePort/SendPort 進行一次事件迴圈,在下一次迴圈時呼叫對應的 callback,具體的呼叫過程在 Future 那篇裡面有說。然後,當我們呼叫Future.microtask建立一個 Future 時,此時它的 callback 就是被新增到 microtask 佇列上的,那麼 callback 的呼叫時機就會被提前,下面就簡單看下一個 microtask 的執行過程。

建立任務

首先,當我們使用 Future 建立一個 microtask 時,呼叫的方法為:

// sdk/lib/async/future.dart
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;
}

// sdk/lib/async/schedule_microtask.dart
@pragma('vm:entry-point', 'call')
void scheduleMicrotask(void Function() callback) {
  _Zone currentZone = Zone._current;
  if (identical(_rootZone, currentZone)) {
    // No need to bind the callback. We know that the root's scheduleMicrotask
    // will be invoked in the root zone.
    _rootScheduleMicrotask(null, null, _rootZone, callback);
    return;
  }
  _ZoneFunction implementation = currentZone._scheduleMicrotask;
  if (identical(_rootZone, implementation.zone) &&
      _rootZone.inSameErrorZone(currentZone)) {
    _rootScheduleMicrotask(
        null, null, currentZone, currentZone.registerCallback(callback));
    return;
  }
  Zone.current.scheduleMicrotask(Zone.current.bindCallbackGuarded(callback));
}
複製程式碼

結構跟呼叫 Timer 基本一致,這裡主要是呼叫 scheduleMicrotask 去建立一個 microtask 的;在 scheduleMicrotask 中根據 Zone 型別不同,選擇了不同的方式建立 microtask,一般情況下,就直接看 RootZone 的實現就可以了,預設情況下,程式碼就是跑在 RootZone 上的。

// sdk/lib/async/zone.dart
void _rootScheduleMicrotask(
    Zone? self, ZoneDelegate? parent, Zone zone, void f()) {
  if (!identical(_rootZone, zone)) {
    bool hasErrorHandler = !_rootZone.inSameErrorZone(zone);
    if (hasErrorHandler) {
      f = zone.bindCallbackGuarded(f);
    } else {
      f = zone.bindCallback(f);
    }
  }
  _scheduleAsyncCallback(f);
}

// sdk/lib/async/schedule_microtask.dart
void _scheduleAsyncCallback(_AsyncCallback callback) {
  _AsyncCallbackEntry newEntry = new _AsyncCallbackEntry(callback);
  _AsyncCallbackEntry? lastCallback = _lastCallback;
  if (lastCallback == null) {
    _nextCallback = _lastCallback = newEntry;
    if (!_isInCallbackLoop) {
      _AsyncRun._scheduleImmediate(_startMicrotaskLoop);
    }
  } else {
    lastCallback.next = newEntry;
    _lastCallback = newEntry;
  }
}
複製程式碼

之後呼叫到 _scheduleAsyncCallback,這個函式是比較重要的,_nextCallback 就是 microtask queue 的的隊首,_lastCallback 則是隊尾,這個函式的作用就是將新的 callback 新增到 microtask 佇列中,同時,當此前還沒有新增過 microtask 的時候,就需要呼叫_AsyncRun._scheduleImmediate(_startMicrotaskLoop);開始一輪 microtask 的執行,_scheduleImmediate 是一個 external 函式,其實現在 sdk/lib/_internal/vm/lib/schedule_microtask_patch.dart 中,

@patch
class _AsyncRun {
  @patch
  static void _scheduleImmediate(void callback()) {
    final closure = _ScheduleImmediate._closure;
    if (closure == null) {
      throw new UnsupportedError("Microtasks are not supported");
    }
    closure(callback);
  }
}

typedef void _ScheduleImmediateClosure(void callback());

class _ScheduleImmediate {
  static _ScheduleImmediateClosure? _closure;
}

@pragma("vm:entry-point", "call")
void _setScheduleImmediateClosure(_ScheduleImmediateClosure closure) {
  _ScheduleImmediate._closure = closure;
}
複製程式碼

這裡的大致流程就是,給 _scheduleImmediate 傳一個 callback,也就是 _startMicrotaskLoop 函式,不過 _scheduleImmediate 也只是轉手將 callback 給了 _ScheduleImmediate._closure 執行,但是 _ScheduleImmediate._closure 是通過 _setScheduleImmediateClosure 賦值的,所以這裡還需要再看 _setScheduleImmediateClosure 是何時被呼叫。從宣告看,這個函式應該是要在 dart vm 中呼叫的,在 vm 程式碼中搜尋找到,在進行 isolate 初始化時,會依此呼叫

  1. DartIsolate::LoadLibraries
  2. DartRuntimeHooks::Install
  3. InitDartAsync

_setScheduleImmediateClosure 就是在這裡被呼叫的,

// dart_runtime_hooks.cc
static void InitDartAsync(Dart_Handle builtin_library, bool is_ui_isolate) {
  Dart_Handle schedule_microtask;
  if (is_ui_isolate) {
    schedule_microtask =
        InvokeFunction(builtin_library, "_getScheduleMicrotaskClosure");
  } else {
    Dart_Handle isolate_lib = Dart_LookupLibrary(ToDart("dart:isolate"));
    Dart_Handle method_name =
        Dart_NewStringFromCString("_getIsolateScheduleImmediateClosure");
    schedule_microtask = Dart_Invoke(isolate_lib, method_name, 0, NULL);
  }
  Dart_Handle async_library = Dart_LookupLibrary(ToDart("dart:async"));
  Dart_Handle set_schedule_microtask = ToDart("_setScheduleImmediateClosure");
  Dart_Handle result = Dart_Invoke(async_library, set_schedule_microtask, 1,
                                   &schedule_microtask);
  PropagateIfError(result);
}
複製程式碼

在這裡,給 set_schedule_microtask 傳的引數時 schedule_microtask,這個函式則是來自於名為 _getIsolateScheduleImmediateClosure 的函式,且這就是一個 dart 函式,直接搜尋,便可以在 sdk/lib/_internal/vm/lib/isolate_patch.dart 找到函式定義,

void _isolateScheduleImmediate(void callback()) {
  assert((_pendingImmediateCallback == null) ||
      (_pendingImmediateCallback == callback));
  _pendingImmediateCallback = callback;
}

@pragma("vm:entry-point", "call")
void _runPendingImmediateCallback() {
  final callback = _pendingImmediateCallback;
  if (callback != null) {
    _pendingImmediateCallback = null;
    callback();
  }
}

/// The embedder can execute this function to get hold of
/// [_isolateScheduleImmediate] above.
@pragma("vm:entry-point", "call")
Function _getIsolateScheduleImmediateClosure() {
  return _isolateScheduleImmediate;
}
複製程式碼

從而得知,_ScheduleImmediate._closure 就是 _isolateScheduleImmediate,所以,callback(也就是 _startMicrotaskLoop)最後作為 _isolateScheduleImmediate 的引數呼叫,也就是把它賦值給 _pendingImmediateCallback。

執行任務

接著看 microtask 的執行,從上面得知,_pendingImmediateCallback 就是 _startMicrotaskLoop,而且 _pendingImmediateCallback 在 _runPendingImmediateCallback 函式中被呼叫,也就是說,當 _runPendingImmediateCallback 被呼叫時,便會啟動新一輪 microtask 的執行。

看到 _runPendingImmediateCallback 這個名字是否有點眼熟,之前在介紹 Future 的時候,通過檢視程式碼,瞭解到 dart vm 中處理訊息事件,最終會呼叫 dart 中的 _handleMessage 函式,

@pragma("vm:entry-point", "call")
static void _handleMessage(Function handler, var message) {
  // TODO(floitsch): this relies on the fact that any exception aborts the
  // VM. Once we have non-fatal global exceptions we need to catch errors
  // so that we can run the immediate callbacks.
  handler(message);
  _runPendingImmediateCallback();
}
複製程式碼

在 dart 中,所有的程式碼其實都是通過這裡呼叫的,不管是當我們啟動一個 Isolate,還是通過 Future 執行非同步呼叫。比如當我們直接啟動 dart 時,它會在 vm 中先建立好一個 isolate,然後執行它的 entry point,對於 root isolate,它的 entry point 就是 main,對於自定義的 isolate,它的 entry point 就是外部傳入的函式,而執行 entry point 的方式,就是通過事件佇列,可以看下面這段程式碼:

@pragma("vm:entry-point", "call")
void _startIsolate(
    Function entryPoint, List<String>? args, Object? message, bool isSpawnUri) {
  _delayEntrypointInvocation(entryPoint, args, message, isSpawnUri);
}

void _delayEntrypointInvocation(Function entryPoint, List<String>? args,
    Object? message, bool allowZeroOneOrTwoArgs) {
  final port = RawReceivePort();
  port.handler = (_) {
    port.close();
    if (allowZeroOneOrTwoArgs) {
      if (entryPoint is _BinaryFunction) {
        (entryPoint as dynamic)(args, message);
      } else if (entryPoint is _UnaryFunction) {
        (entryPoint as dynamic)(args);
      } else {
        entryPoint();
      }
    } else {
      entryPoint(message);
    }
  };
  port.sendPort.send(null);
}
複製程式碼

當 isolate 在 vm 中建立好後,c++ 就會呼叫 _startIsolate 啟動 isolate,而在這個函式中,它依舊是通過 ReceivePort/SendPort 向事件佇列中傳送一個事件,以此來執行 entryPoint。

以上文字,僅為說明 _handleMessage 這個函式的呼叫時機,同時也是在說明 _runPendingImmediateCallback 的呼叫時機,也就是 _startMicrotaskLoop。

// sdk/lib/async/schedule_microtask.dart
void _startMicrotaskLoop() {
  _isInCallbackLoop = true;
  try {
    // Moved to separate function because try-finally prevents
    // good optimization.
    _microtaskLoop();
  } finally {
    _lastPriorityCallback = null;
    _isInCallbackLoop = false;
    if (_nextCallback != null) {
      _AsyncRun._scheduleImmediate(_startMicrotaskLoop);
    }
  }
}

void _microtaskLoop() {
  for (var entry = _nextCallback; entry != null; entry = _nextCallback) {
    _lastPriorityCallback = null;
    var next = entry.next;
    _nextCallback = next;
    if (next == null) _lastCallback = null;
    (entry.callback)();
  }
}
複製程式碼

這個函式的實現還是挺簡單的,就是直接遍歷 microtask 佇列去執行,有一點,當我們在 micro task 執行過程中再建立 microtask 的時候,由於此時 _microtaskLoop 還未結束,所以當這個 microtask 執行完之後,會繼續執行新加的 microtask。不過在 _startMicrotaskLoop 中執行 _microtaskLoop 外面加了一個 try...finally 倒是沒太理解這裡的用途,因為從這裡直到 _handleMessage 都沒見到捕獲異常的操作。

總的來說,以上就是 microtask 從建立到執行的過程,下面具體講講幾種不同的程式碼塊具體的執行時機。

microtask 執行時機

從當前的資訊瞭解到,在一個連續的程式碼中,可以有三種形式的程式碼呼叫,

  1. 直接呼叫
  2. 通過 microtask 呼叫
  3. 通過事件佇列呼叫

這三種形式的呼叫,其執行先後為直接呼叫->microtask->事件佇列, 從之前的分析中就很容易理解了,下面再總結一下。

首先,直接呼叫很好理解,他們都是同在一個函式下,按順序呼叫。

然後,microtask 的呼叫是發生在當前的訊息事件呼叫之後(從 _handleMessage 實現可知),而從之前的分析中得知,所有的關於 dart 程式碼的呼叫,其入口都是 _handleMessage,直接執行 main 函式的時候是,新啟動一個 isolate 也是,而 _handleMessage 下有兩個入口,一個是 handler,一個是 _runPendingImmediateCallback,而不管 microtask 是在這兩個函式的哪一個中建立的,都會在本次 _handleMessage 呼叫過程中執行。

而第三種,訊息事件呼叫就不一樣了,它一定是要等到下次甚至好幾次之後的 _handleMessage 中才會呼叫,相比之下,它的呼叫很昂貴,需要經過 dart vm 的一層處理才能呼叫到。

總結,基於 dart 的單執行緒事件迴圈模型,我們可以將 _handleMessage 看作一次事件迴圈,那麼「直接呼叫」與「microtask」都是在當次 _handleMessage 的呼叫中就會呼叫到,而「事件佇列呼叫」則至少要等到下次 _handleMessage 呼叫,由此決定了這三種程式碼呼叫的執行順序。

相關文章