flutter“多執行緒”isolate小學生級理解

jethroHuang發表於2020-03-29

我們小學二年級就學過:

Dart 是單執行緒的並且 Flutter 依賴於 Dart。

但是如果我們要在主執行緒做一些計算量大的操作,就必然會阻塞執行緒,使UI更新卡頓甚至卡死。那怎麼辦呢?

好訊息是 Dart 為我們提供了 isolate,isolate 跟執行緒差不多,他是 Dart 中的執行緒。

isolate 與執行緒的區別就是執行緒與執行緒之間是共享記憶體的,而 isolate 和 isolate 之間是不共享的,所以叫 isolate (隔離)。

在flutter 裡面主執行緒就是主 isolate 。如果我們要進行一些大計算量的操作就應該啟動一個新的 isolate。

那麼應該如何來開啟呢?在此之前我想講個故事。

小紅與小藍的故事

有個舞者叫小紅,她正在給觀眾跳舞。

跳舞的小紅

但是觀眾卻要求她一邊跳舞一邊計算一個數字裡面有多少個偶數。於是。。。

非同步計算

這那行啊!你必須給我一邊跳一邊算,算的時候不能停下來!

於是小紅沒辦法,決定在異世界召喚一個小藍來幫她計算。

flutter“多執行緒”isolate小學生級理解

但是小紅和小藍被異世界的屏障隔離,她們也沒有思想共通的超能力。只能在召喚的同時傳送一個包裹給小藍

小藍被召喚出來後收到包裹,開啟后里面是要計算的數字,就開始計算,但是計算後要怎麼把結果告訴小紅呢?

上帝做了一個約定,在小紅召喚小藍的時候,會變一個傳送裝置(傳送裝置可以用來接收包裹,還可以生成一個專屬傳送器)。然後把傳送器傳送給小藍。

當小藍被召喚出來後,開啟包裹,裡面是一個傳送器,然後小藍自己也變一個傳送裝置,生成一個傳送器,然後用小紅的傳送器把小藍的傳送器傳送給小紅。傳送出去後就坐在傳送裝置旁邊等包裹。

當小紅收到小藍的傳送器後就把小藍的傳送器存起來。

當有觀眾要求小紅計算時,就分神一邊跳舞,一邊生成一個臨時傳送裝置,把要計算的數字和臨時傳送器打包成一個包裹,然後通過小藍的傳送器發給小藍,等傳送裝置出結果。因為不用自己算了,只是等,所以跳舞的時候線條也流暢了,動作也優美了。

說回小藍這邊,小藍看到傳送裝置出現了一個包裹,裡面是一個臨時傳送器,還有一個數字。於是小藍就開始計算。算好了就用臨時傳送器把數字傳送給小紅。

小紅收到結果後就告訴觀眾,那個數字有多少個偶數。

故事結束,第一次嘗試這樣的風格,可能寫得有點爛,不過結合程式碼來看的話,應該還是挺容易理解的。

程式碼實踐

首先我們先讓小紅跳起舞來。

  @override
  void initState() {
    controller =
        AnimationController(duration: Duration(seconds: 3), vsync: this);
    animation = Tween<double>(begin: 0, end: pi * 2).animate(controller);
    controller.repeat();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedBuilder(
                animation: animation,
                child: Text(
                  '小紅',
                  style: TextStyle(fontSize: 30, color: Colors.red),
                ),
                builder: (context, child) {
                  return Transform.rotate(
                    angle: animation.value,
                    child: child,
                  );
                }),
          ],
        ),
      ),
    );
  }
複製程式碼

接下來讓小紅計算一個數字裡面有多少個偶數。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          AnimatedBuilder(
              animation: animation,
              child: Text(
                '小紅',
                style: TextStyle(fontSize: 30, color: Colors.red),
              ),
              builder: (context, child) {
                return Transform.rotate(
                  angle: animation.value,
                  child: child,
                );
              }),
          Padding(
            padding: EdgeInsets.only(top: 16),
            child:
                RaisedButton(onPressed: count, child: Text('非同步計算偶數的個數')),
          ),
          Text(result)
        ],
      ),
    ),
  );
}

int getRandom() {
  int a =  Random().nextInt(100);
  return a + 1000000000;
}

// 非同步計算
count() async {
  int random = getRandom();
  int r = countEven(random);
  setState(() {
    this.result = '${random.toString()}${r.toString()}個偶數';
  });
}

//計算偶數的個數
int countEven(num num) {
  int count = 0;
  while (num > 0) {
    if (num % 2 == 0) {
      count++;
    }
    num--;
  }
  return count;
}

複製程式碼

這就是效果

非同步計算

定義 isolate

我願稱之為召喚小藍。

首先我們要知道兩個類:

ReceivePort
SendPort
複製程式碼

ReceivePort 就是故事中的傳送裝置,而 SendPort 則是傳送器。

我們可以通過以下方式建立傳送裝置和對應的傳送器

ReceivePort receive = ReceivePort();
SendPort sender = receive.sendPort;
複製程式碼

好的,知道這些就行了。接下來我們定義小藍。

// 訊息包裹,用來存臨時傳送器和訊息
class MessagePackage {
  SendPort sender; // 臨時傳送器
  dynamic msg; // 訊息

  MessagePackage(this.sender, this.msg);

}

// 我是小藍,負責計算偶數的個數,我必須是頂級函式
blueCounter(SendPort redSendPort) {
  // 建立小藍的傳送裝置
  ReceivePort blueReceivePort = ReceivePort();
  // 用小紅的傳送器把小藍的傳送器傳送出去
  redSendPort.send(blueReceivePort.sendPort);
  // 監聽小藍的傳送裝置,等待小紅叫小藍計算
  blueReceivePort.listen((package) {
    // 這裡的msg是dynamic,需要轉換成 MessagePackage 類,上面自己定義的包裹封裝類
    MessagePackage _msg = package as MessagePackage;
    // 小藍開始計算
    int r = countEven(_msg.msg as num);
    // 計算好了用小紅的臨時傳送器告訴小紅
    _msg.sender.send(r);
  });
}

複製程式碼

建立isolate

工具人小藍定義好了,我們去初始化(召喚)一下小藍。

// 建立isolate
void createIsolate() async {
  // 建立小紅的接收器,用來接收小藍的傳送器
  ReceivePort redReceive = ReceivePort();
  // 建立 isolate, 並且把小紅的傳送器傳給小藍
  isolate = await Isolate.spawn<SendPort>(blueCounter, redReceive.sendPort);
  // 等待小藍把傳送器傳送給小紅
  blueSender = await redReceive.first;
  // 不用了記得關閉接收器
  redReceive.close();
}


@override
void initState() {
  controller =
      AnimationController(duration: Duration(seconds: 3), vsync: this);
  animation = Tween<double>(begin: 0, end: pi * 2).animate(controller);
  controller.repeat();
  // 在initState中初始化isolate
  createIsolate();
  super.initState();
}
複製程式碼

現在小藍已經被召喚了出來,並且和小紅建立了通訊。

使isolate 開始計算

接下來我們就讓小紅開始計算吧。


@override
Widget build(BuildContext context) {
  ...
  Padding(
      padding: EdgeInsets.only(top: 16),
      child: RaisedButton(
          onPressed: isolateCount, child: Text('isolate計算偶數的個數')
      ),
  ),
  ...
}

// 開啟isolate計算
isolateCount() async {
  // 獲取要計算的數字
  int random = getRandom();
  // 建立一個臨時傳送裝置
  ReceivePort _temp = ReceivePort();
  // 用小藍的傳送裝置傳送一個訊息包裹,裡面是臨時傳送裝置的傳送器和要計算的數字
  blueSender.send(MessagePackage(_temp.sendPort, random));
  // 等待臨時傳送裝置返回計算結果
  int r = await _temp.first;
  // 不用了記得關閉臨時接收器
  _temp.close();
  // 把計算結果告訴觀眾
  setState(() {
    this.result = '${random.toString()}${r.toString()}個偶數';
  });
}
複製程式碼

需要注意的是當使用完了 isolate 記得要銷燬。


@override
void dispose() {
  // 銷燬 isolate
  isolate?.kill(priority: Isolate.immediate);
  super.dispose();
}
複製程式碼

OK,到這裡相信你已經看懂並會使用 isolate 了。 我們來看看效果圖。

isolate計算

使用 computed

到這裡還沒完,也許你會覺得太麻煩了。是的這樣用 isolate 太麻煩了,isolate 被設計成可以多次輸入輸出,而我們做這個計算只有一次輸入和輸出,那麼我們就可以用 flutter 為我們提供的 computed 來完成計算操作,它是對 isolate 的一個封裝。下面看看怎麼用吧!敲簡單的。

import 'dart:isolate';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:math';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'isolate Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'isolate Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;
  String result = '';
  SendPort blueSender;
  Isolate isolate;

  @override
  void initState() {
    controller =
        AnimationController(duration: Duration(seconds: 3), vsync: this);
    animation = Tween<double>(begin: 0, end: pi * 2).animate(controller);
    controller.repeat();
    // 在initState中初始化isolate
    createIsolate();
    super.initState();
  }

  @override
  void dispose() {
    // 銷燬 isolate
    isolate?.kill(priority: Isolate.immediate);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            AnimatedBuilder(
                animation: animation,
                child: Text(
                  '小紅',
                  style: TextStyle(fontSize: 30, color: Colors.red),
                ),
                builder: (context, child) {
                  return Transform.rotate(
                    angle: animation.value,
                    child: child,
                  );
                }),
            Padding(
              padding: EdgeInsets.only(top: 16),
              child: RaisedButton(onPressed: count, child: Text('非同步計算偶數的個數')),
            ),
            Padding(
              padding: EdgeInsets.only(top: 16),
              child: RaisedButton(
                  onPressed: isolateCount, child: Text('isolate計算偶數的個數')),
            ),
            Padding(
              padding: EdgeInsets.only(top: 16),
              child: RaisedButton(
                  onPressed: computeCount, child: Text('compute計算偶數的個數')),
            ),
            Text(result)
          ],
        ),
      ),
    );
  }

  // 獲取隨機數
  int getRandom() {
    int a = Random().nextInt(100);
    return a + 1000000000;
  }

  // 非同步計算
  count() async {
    int random = getRandom();
    int r = countEven(random);
    setState(() {
      this.result = '${random.toString()}${r.toString()}個偶數';
    });
  }

  // 建立isolate
  void createIsolate() async {
    // 建立小紅的接收器,用來接收小藍的傳送器
    ReceivePort redReceive = ReceivePort();
    // 建立 isolate, 並且把小紅的傳送器傳給小藍
    isolate = await Isolate.spawn<SendPort>(blueCounter, redReceive.sendPort);
    // 等待小藍把傳送器傳送給小紅
    blueSender = await redReceive.first;
    // 不用了記得關閉接收器
    redReceive.close();
  }

  // 利用compute計算
  computeCount() async {
    int random = getRandom();
    // compute 的回撥函式必須是頂級函式或者static函式
    int r = await compute(countEven, random);
    setState(() {
      this.result = '${random.toString()}${r.toString()}個偶數';
    });
  }

  // 開啟isolate計算
  isolateCount() async {
    // 獲取要計算的數字
    int random = getRandom();
    // 建立一個臨時傳送裝置
    ReceivePort _temp = ReceivePort();
    // 用小藍的傳送裝置傳送一個訊息包裹,裡面是臨時傳送裝置的傳送器和要計算的數字
    blueSender.send(MessagePackage(_temp.sendPort, random));
    // 等待臨時傳送裝置返回計算結果
    int r = await _temp.first;
    _temp.close();
    // 把計算結果告訴觀眾
    setState(() {
      this.result = '${random.toString()}${r.toString()}個偶數';
    });
  }
}

// 訊息包裹,用來存臨時傳送器和訊息
class MessagePackage {
  SendPort sender; // 臨時傳送器
  dynamic msg; // 訊息

  MessagePackage(this.sender, this.msg);
}

// 我是小藍,負責計算偶數的個數,我必須是頂級函式
blueCounter(SendPort redSendPort) {
  // 建立小藍的傳送裝置
  ReceivePort blueReceivePort = ReceivePort();
  // 用小紅的傳送器把小藍的傳送器傳送出去
  redSendPort.send(blueReceivePort.sendPort);
  // 監聽小藍的傳送裝置,等待小紅叫小藍計算
  blueReceivePort.listen((package) {
    // 這裡的msg是dynamic,需要轉換成 MessagePackage 類,上面自己定義的包裹封裝類
    MessagePackage _msg = package as MessagePackage;
    // 小藍開始計算
    int r = countEven(_msg.msg as num);
    // 計算好了用小紅的臨時傳送器告訴小紅
    _msg.sender.send(r);
  });
}

//計算偶數的個數,此函式需要大量的計算資源和時間
int countEven(num num) {
  int count = 0;
  while (num > 0) {
    if (num % 2 == 0) {
      count++;
    }
    num--;
  }
  return count;
}
複製程式碼

相關文章