dart 07.3 併發多線程

什麼是isolate

dart 雖然是一個單線程語言 但是不代表他不支持多線程併發

  • 在dart中線程不叫線程叫做isolate(隔離區)所有的代碼都運行在這
    • 類似於線程但不共享內存的獨立工作程序,僅通過消息進行通信。
    • 每個isolate 都有一個完整的事件循環機制,每個隔離區都有自己的內存堆,確保每個隔離區的狀態都不會被其他隔離區訪問。
      • 這意味着在一個 Isolate 中運行的代碼與另外一個 Isolate不存在任何關聯。
      • 依賴這點所以我們通過Isolate實現併發

名詞解釋

  • 在下面描述中
    • mainIsolate代表調用Isolate或者叫主Isolate
    • newIsolate 代表**新創建的Isolate**
    • mainReceivePort 代表**調用IsolateReceivePort**
    • newReceivePort 代表**新IsolateReceivePort**
    • mainSendPort 代表**調用IsolateSendPort**
    • newSendPort 代表**新IsolateSendPort**

實現Isolate

mainIsolate中創建newIsolate有兩種方法 spawn,spawnUri

創建步驟

  1. 創建newIsolate並與當前調用mainIsolate並通過sendPort建立連接
    • 因爲Isolate之間不共享內存且通過消息交互,所以**Isolate之間通訊需要持有對方發送消息的端口,文檔裏叫**SendPort
      • 對於sendPort文檔裏這樣描述:[SendPort]s are created from [ReceivePort]s.
      • 翻譯:發送端口是從接收端口創建的
      • RecivePort文檔裏這樣描述:Together with SendPort, the only means of communication between isolates.
      • 與發送端一起,是一種isolate之間的通訊方式
      • 所以Isolate之間通訊需要持有對方的SendPort
    • 創建newIsolate
    • 獲得mainIsolatemainRecivePortmainSendPort
      • 要將mainIsolatemainSendPort發送到newIsolate
      • mainRecivePort 監聽 mainSendPort 發送回來的數據
      • 至於如何將mainSendPort發送到newIsolate的請看下面的例子
  2. 發送數據
    • 通過各自的sendPort.send
  3. 接收數據通過
    • 各自的RecivePort
  4. 在合適的機會銷燬Isolate

下面看具體的步驟

spawn

  • spawn創建並生成與當前Isolate共享相同代碼的Isolate。

  • Future<Isolate> spawn<T>(void entryPoint(T message), T message)

    • 可以看到每一個isolate 需要兩個必要參數
      • 入口點函數 entryPoint
        • 文檔對其這樣描述
        • 參數entryPoint指定要在派生的隔離中調用的初始函數,
        • 入口點函數在新的Isolate中被調用,
      • message只有[message]作爲entryPoint的參數。
        • message通常用於傳送調用IsolateSendPort對象
    • entryPoint函數就是要在**newIsolate**中運行的函數
    • 因爲Isolate相互通訊需要持有對方的sendport
      • 所以我們將**mainIsolatemainSendPort** 作爲**message傳遞到將要在newIsolate** 中執行的入口函數(entryPoint)中,使 newIsolate 持有mainSendPort
      • 入口函數在**newIsolate** 中執行,我們再通過**mainSendPort** 將**NewIsolatenewSendPort** 發送到**mainIsolate**中 ,使 mainIsolate持有newSendPort
      • 這樣**mainIsolate持有了newSendPort** ,
        • 這樣**mainIsolate通過newSendPort** 可以將**mainIsolate中的消息發送到newIsolate**
      • 這樣**newIsolate持有了mainSendPort**
        • 這樣**newIsolate通過mainSendPort** 可以將**newIsolate中的消息發送到mainIsolate**
      • 所以是通過入口函數將**主Isolate的SendPort發送到Newisolate**
    • 需要注意 isolate 的「入口函數(entryPoint)」必須是頂級函數或靜態方法。
  • 示例代碼

  • import 'dart:io';
    import 'dart:isolate';
    main() async {
      print("main start");
      createIsolate();
      print("main end");
    }
    
    Isolate newIsolate;
    
    void createIsolate() async {
      // 兩個Isolate要想互相通訊須持有對方的的sendPort
      // 獲取mainIsolate的監聽器 mainReceivePort
      ReceivePort main_rp = ReceivePort();
      // 獲取 mainIsolate 的 SendPort 並作爲參數傳遞給newIsolate
      // 使 newIsolate 持有 mainSendPort,用於通訊
      // 使 newIsolate 可以通過 mainSendPort 將 newIsolate 的發送消息回 mainIsolate
      SendPort main_send = main_rp.sendPort;
      // 創建新的isolate
      newIsolate = await Isolate.spawn(excuter, main_send);
      // 這裏需要得到 newIsolate 的 SendPort,
      // 讓 mainIsolate 持有 newSendPort,用於通訊
      // 使 mainIsolate 可以通過 newSendPort 將 mainIsolate 的發送消息回 newIsolate
      // 注意 這裏 newSendPort 是 newIsolate中的mainSendPort 發送回來的所以要在監聽中獲取newSendPort
      SendPort new_send;
      //主接收器(mainReceivePort)開始監聽newIsolate中的mainSendPort發送回來的消息
      main_rp.listen((message) {
        print("NewIsolat通過main_send發送來一條消息 $message ,到主Isolate");
        if (message[0] == 0) {
          // 獲取newSendPort
          new_send = message[1] as SendPort;
        } else {
          new_send?.send("mian_isolate 通過new_send發送了一條消息到NewIsolate");
        }
      });
    }
    
    // 入口函數將在newIsolate中執行
    void excuter(SendPort mainSendPort) {
      // 獲取newIsolate的監聽器newReceivePort
      ReceivePort new_rp = ReceivePort();
      //newReceivePort開始監聽 mainIsolate中的newSendPort發送回來的消息
      new_rp.listen((message) {
        print(message);
        // 接收到第一條main發送過來的函數 就銷燬newIsolate
        print("銷燬NewIsolate");
        destroyNewIsolate();
      });
      // 獲取newIsolate的 SendPort 
      SendPort new_send = new_rp.sendPort;
      //將其發送到 mainIsolate
      // 讓 mainIsolate 持有 newSendPort,用於通訊
      // 使 mainIsolate 可以通過 newSendPort 將 mainIsolate 的發送消息回 newIsolate
      mainSendPort.send([0, new_send]);
      // 模擬耗時5秒
      sleep(Duration(seconds: 5));
      mainSendPort.send([1, "excuter 任務完成"]);
      print("NewIsolat 執行結束");
    }
    
    //銷燬newIsolate
    destroyNewIsolate() {
      // 任務執行結束銷燬newIsolate
      newIsolate?.kill(priority: Isolate.immediate);
      newIsolate = null;
    }
    /* 輸出
    newIsolat通過main_send發送來一條消息 [0, SendPort] ,到mainIsolate
    newIsolat通過main_send發送來一條消息 [1, excuter 任務完成] ,到mainIsolate
    NewIsolat 執行結束
    mianIsolate 通過new_send發送了一條消息到newIsolate
    銷燬NewIsolate
    */
    
    

spawn Uri

  • 創建並派生一個Isolate,該Isolate使用指定的URI從庫中運行代碼。

  • Isolate.spawnUri(Uri uri,List<String> args,var message);`

  • spawnUri方法有三個必須的參數,

    • 第一個是Uri,指定一個新Isolate代碼文件的路徑,
    • 第二個是參數列表,類型是List,
    • 第三個是消息。其實是發送消息的端口SendPort
  • 注意這種方式

    • newIsolate代碼在一個單獨的文件裏

    • newIsolate的的執行函數,必須包是一個main函數,它是newIsolate的入口方法,

    • main函數必須可以用零個一個或者兩個參數調用

      • `main()`
        `main(args)`
        `main(args, message)`
        
      • main函數中的args參數列表,正對應spawnUri中的第二個參數。如不需要向newIsolate中傳參數,該參數可傳空List

      • message則是調用Isolate的SendPort

  • 示例代碼

    • mainIsolate

      • import 'dart:isolate';
        
        Isolate newIsolate;
        
        main() async {
          ReceivePort mainReceivePort = ReceivePort();
          SendPort mainSendPort = mainReceivePort.sendPort;
          List<String> list = ["hello, isolate", "this is args"];
          var uri = Uri(path: "./newTaskUri.dart");
          // 創建newIsolate 並建立連接
          newIsolate = await Isolate.spawnUri(uri, list, mainSendPort);
          // 需要獲取 newSendPort 用於通訊
          // newSendPort 是 newIsolate中的mainSendPort 發送回來的所以要在監聽中獲取結果
          SendPort newSendPort;
          mainReceivePort.listen((message) {
            print("newIsolat通過main_send發送來一條消息 $message ,到mainIsolate");
            if ("excuter 任務完成" == message[1]) {
              // 銷燬newIsolate
              print("銷燬newIsolate");
              destroyNewIsolate();
        
            }
            if (message[0] == 0) {
              // 獲取newSendPort
              newSendPort = message[1] as SendPort;
            } else {
              newSendPort?.send("mian_isolate 通過new_send發送了一條消息到newIsolate");
            }
          });
        }
        
        //銷燬newIsolate
        destroyNewIsolate() {
          // 任務執行結束銷燬newIsolate
          newIsolate?.kill(priority: Isolate.immediate);
          newIsolate = null;
        }
        
        
    • newIsolate文件newTaskUri.dart

      • import 'dart:io';
        import 'dart:isolate';
        
        // 這裏的main 就是入口函數 在newIsolate中執行
        // 就相當與 spawn中的 excuter
        // 內部執行回傳sendport,消息監聽發送,邏輯是一樣的
        // 區別就是多了一個參數列表可以傳一些參數處理些邏輯 功能更豐富了
        void main(args, SendPort mainSendPort) {
          try {
            print("newIsolate 開始");
            print("newIsolate (參數列表)args: $args");
        
            ReceivePort newRecivePort = new ReceivePort();
            //newReceivePort開始監聽 newSendPort發送回來的消息
            newRecivePort.listen((message) {
              print(message);
              // 接收到第一條消息
            });
            // 獲取newSendPort 並通過mainSendPort 回傳到mainIsolate
            SendPort newSendPort = newRecivePort.sendPort;
            mainSendPort.send([0, newSendPort]);
            // 模擬耗時5秒
            sleep(Duration(seconds: 5));
            mainSendPort.send([1, "excuter 任務完成"]);
            print("NewIsolat 執行結束");
          } catch (e) {
            print("myerr $e");
          }
        }
        
    • 輸出結果

      • /**
        newIsolate 開始
        newIsolate (參數列表)args: [hello, isolate, this is args]
        newIsolat通過main_send發送來一條消息 [0, SendPort] ,到mainIsolate
        NewIsolat 執行結束
        newIsolat通過main_send發送來一條消息 [1, excuter 任務完成] ,到mainIsolate
        銷燬newIsolate
        */
        
      • spawn輸出結果對比少了一個mainIsolatnewIsolate發送消息,是因爲代碼中銷燬newIsolate時機不同

        • spawnnewIsolate執行結束後mainIsolatenewIsolate發送消息且被處理後銷燬newIsolate
        • spawnUrinewIsolate執行結束後就銷燬了newIsolate

需要注意

  1. 無論是上面的**spawn還是spawnUri**,運行後都會創建兩個Isolate

    • spawn創建並生成與當前Isolate共享相同代碼的新Isolate。
    • spawnUri 一個獨立的不與調用Isolate共享代碼的新Isolate
  2. 想要相互通訊就必須持有對方的SendPort

    • newIsoalte 要持有mainSendPort 靠創建Isolate是入口點函數傳參 參數即`mainSendPort``
    • ``mainIsolate 要持有newSendPort靠傳入newIsolatemainSendPort發送到mainIsolate`
  3. 釋放newIsolate

    • 當我們使用完自己創建的Isolate之後,最好調用kill將Isolate殺死,否則Isolate 會一直存在造成內存消耗
  4. Platform-Channel 通信僅僅主 isolate 支持。該主 isolate 對應於應用啓動時創建的 isolate

    • 也就是說,通過編程創建的 isolate 實例,無法實現 Platform-Channel 通信 這難道是

區別

  • swpanUrinewIsolate 必須在單獨的文件裏,又因爲必須有main函數作爲入口,所以程序會出現兩個main

  • 在flutter環境下一直又一個main的錯誤未處理 我猜測可能是因爲兩個main函數引起的,也有可能是上面第四點原因導致的如果有人知道請評論告訴我下 謝謝了

  • spawn 通常我們newIsolatemainIsolate寫在同一個文件,也不會出現兩個main函數方便管理Isolate

  • 在flutter環境下spwan創建可以正常執行

    • spwan要注意入口點函數必須是頂層函數或者靜態函數

在flutter中創建Isolate

可以看到 無論是實現上述哪一種isolate 代碼數量都是比較繁瑣的

對此Flutter提供了一個函數**compute** 該函數封裝了通過spawn實現Isolate的代碼

避免我們去寫通過spawn創建Isolate的一系列代碼,

直接通過compute函數,這樣讓我們的代碼看起來更簡潔了

compute

如果任務只是進行一次計算返回結果,不需要雙端多次溝通的話 使用compute 函數將非常簡單

compute<Q, R>(ComputeCallback<Q, R> callback, Q message)

  • compute函數有兩個必須的參數

    • callback 爲執行函數
      • 必須是頂級函數或者靜態方法
    • message爲消息 可以是callback執行函數的參數
  • 示例

    • import 'dart:io';
      
      import 'package:flutter/foundation.dart';
      import 'package:flutter/material.dart';
      
      void main() => runApp(new MyApp());
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return new MaterialApp(
              title: 'ComputeIsolate',
              theme: new ThemeData(
                primarySwatch: Colors.blue,
              ),
              home: Scaffold(
                  appBar: AppBar(title: Text("ComputeIsolate")),
                  body: Column(
                      mainAxisSize: MainAxisSize.min,
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Center(
                          child: FlatButton(
                              onPressed: () async {
                                String s = await compute(work, 4);
                                print(s);
                              },
                              child: Text(
                                "Click",
                              )),
                        ),
                        Center(
                            child: Text(
                          "Click",
                        )),
                      ])));
        }
      }
      String work(num duration) {
        print("work start");
        sleep(Duration(seconds: duration));
        return "$duration 秒後執行結束";
      }
      
      // 運行點擊屏幕 後輸出
      // work start
      // 4 秒後執行結束
      
  • 怎麼樣 是不是很簡單

我們應該什麼時候使用Future和Isolate

  • 耗時較多的任務放到Isolate中
    • 如果你不想你的Ui卡頓或者程序中斷
      • 通過之前的時間循環機制我們瞭解到Future只是將任務放到了Event隊列,還是在當前Isolate 不過是等其他代碼執行結束在執行Event隊列中的任務,而UI渲染交互等又都是在Event隊列中處理,如果我們Future任務耗時過多會導致Ui卡頓甚至整個進程都被中斷,所以我們才需要多Isolate 併發處理任務
    • 直觀的說可以根據任務執行時間長短來區分
      • 代碼段運行時間只要幾十毫秒=>Future
      • 代碼段運行時間要幾百毫秒甚至更久應該用Isolate
        • 比如Io操作

總結

  • Isolate雖然可以併發但是也要考慮適用場景
  • 如果需要使用Isolate 要考慮場景
    • 只執行一次返回結果 不需要多次通訊 使用compute函數
    • 如果需要多次溝通我們可以通過 spawn 來創建Isolate

參考

掘金文章 [譯] Flutter 異步編程:Future、Isolate 和事件循環

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章