dart系列之:dart優秀的秘訣-隔離機制

簡介

之前介紹了很多dart中的異步編程技巧,不知道大家有沒有發現一個問題,如果是在java的異步編程中,肯定會提到鎖和並發機制,但是對於dart來說,好像從來沒有聽到多線程和並發的問題,這是為什麼呢?

今天,給大家講解一下dart中的隔離機制,大家就明白了。

dart中的隔離機制

dart是一個單線程的語言,但是作為一個單線程的語言,dart卻支持Future,Stream等異步特性。這一切都是隔離機制和事件循環帶來的結果。

首先看一下dart中的隔離機制。

所謂隔離指的是dart運行的一個特定的空間,這個空間擁有單獨的內存和單線程的事件循環。

如下圖所示:

在java或者c++等其他語言中,多個線程是共享內存空間的,雖然帶來了並發和數據溝通的方便途徑,但是同時也造成了並發編程的困難。

因為我們需要考慮多線程之間數據的同步,於是額外多出了很多鎖的機制,詳細了解或者用過的人應該都會很煩惱。

多線程最大的缺陷就是要求程序員的羅輯思維和編程技巧足夠優秀,這樣才能夠設計出完美運行的多線程程序。

但是在dart中,這些都不是什麼問題。dart中所有的線程都擁有自己的運行空間,這個線程的工作就是運行事件循環。

那麼問題來了,主線程在處理事件循環,但是如果遇到了一個非常耗時的操作,該怎麼辦呢? 如果直接在主線程中運行,則可能會導致主線程的阻塞。

dart也充分考慮到了這個問題,所以dart提供了一個Isolate的類來對隔離進行管理。

因為dart程序本身就在一個Isolate中運行,所以如果在dart中定義一個Isolate,那麼這個Isolate通常表示的是另外一個,需要和當前Isolate進行通信的Isolate。

生成一個Isolate

那麼如何在當前的dart程序中生成一個Isolate呢?

Isolate提供了三種生成方法。

一個非常常用的是Isolate的工廠方法spawn:

  external static Future<Isolate> spawn<T>(
      void entryPoint(T message), T message,
      {bool paused = false,
      bool errorsAreFatal = true,
      SendPort? onExit,
      SendPort? onError,
      @Since("2.3") String? debugName});

spawn會創建一個新的Isolate,調用它需要傳入幾個參數:

entryPoint表示的是生成新Isolate的時候需要調用的函數。entryPoint接受一個message參數。通常來說message是一個SendPort對象,用於兩個Isolate之間的溝通。

paused表示新生成的Isolate是否處於暫停狀態,他相當於:

isolate.pause(isolate.pauseCapability)

如果後續需要取消暫停狀態,則可以調用:

isolate.resume(isolate.pauseCapability)

errorsAreFatal 對應的是setErrorsFatal方法。

onExit對應的是addOnExitListener, onError對應的是addErrorListener。

debugName表示的是Isolate在調試的時候展示的名字。

如果spawn出錯,則會拋出IsolateSpawnException異常:

class IsolateSpawnException implements Exception {
  /// Error message reported by the spawn operation.
  final String message;
  @pragma("vm:entry-point")
  IsolateSpawnException(this.message);
  String toString() => "IsolateSpawnException: $message";
}

spawn方法生成的是和當前代碼一樣的Isolate。如果想要使用不同的代碼來生成,則可以使用spawnUri,通過傳入對應的Uri地址,從而生成不一樣的code。

external static Future<Isolate> spawnUri(
      Uri uri,
      List<String> args,
      var message,
      {bool paused = false,
      SendPort? onExit,
      SendPort? onError,
      bool errorsAreFatal = true,
      bool? checked,
      Map<String, String>? environment,
      @Deprecated('The packages/ dir is not supported in Dart 2')
          Uri? packageRoot,
      Uri? packageConfig,
      bool automaticPackageResolution = false,
      @Since("2.3")
          String? debugName});

還有一種方式,就是使用Isolate的構造函數:

Isolate(this.controlPort, {this.pauseCapability, this.terminateCapability});

它有三個參數,第一個參數是controlPort,代表另外一個Isolate的控制權,後面兩個capabilities是原isolate的子集,表示是否有pause或者terminate的權限。

一般用法如下:

Isolate isolate = findSomeIsolate();
Isolate restrictedIsolate = Isolate(isolate.controlPort);
untrustedCode(restrictedIsolate);

Isolate之間的交互

所有的dart代碼都是運行在Isolate中的,然後代碼只能夠訪問同一個isolate內的class和value。那麼多個isolate之間通信,可以ReceivePort和SendPort來實現。

先看下SendPort,SendPort是Capability的一種:

abstract class SendPort implements Capability 

SendPort用於向ReceivePort發送message, message可以有很多類型,包括:

Null,bool,int,double,String,List,Map,TransferableTypedData,SendPort和Capability。

注意,send動作是立馬完成的。

事實上,SendPort是由ReceivePort來創建的。一個ReceivePort可以接收多個SendPort。

ReceivePort是Stream的一種:

abstract class ReceivePort implements Stream<dynamic>

作為Stream,它提供了一個listen用來處理接收到的消息:

  StreamSubscription<dynamic> listen(void onData(var message)?,
      {Function? onError, void onDone()?, bool? cancelOnError});

一個例子

講了那麼多原理,有的同學可能會問了,那麼到底怎麼用呢?

例子來了:

import 'dart:isolate';

var isolate;

void entryPoint(SendPort sendPort) {
  int counter = 0;
  sendPort.send("counter:$counter");
}
void main() async{
  final receiver = ReceivePort();
  receiver.listen((message) {
    print( "接收到消息 $message");
  });
  isolate = await Isolate.spawn(entryPoint, receiver.sendPort);
}

在主線程中,我們創建了一個ReceivePort,然後調用了它的listen方法來監聽sendPort發過來的消息。

然後spawn出一個新的Isolate,這個Isolate會在初始化之後,調用entryPoint方法。

在這個entryPoint方法中又使用sendPort向ReceivePort發送消息。

最終運行,打印:

接收到消息 counter:0

總結

以上就是dart中的隔離機制和Isolate的使用。

本文已收錄於 //www.flydean.com/25-dart-isolates/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!