Flutter开发系列(三)--Dart异步机制与异步编程

妖精的绣舞 提交于 2019-12-28 07:01:04

  Dart是单线程语言,但是请求网络,数据操作和IO操作等耗时操作需要异步,否则可能会导致无响应。Isolate机制是Dart中的异步机制。与Java中的线程不同,Isolate中的线程(可以先称之为线程)是内存隔离的(像进程一样的线程),而Java中的线程是内存共享的。

  Dart中的main函数启动时会启动一个进程(可先称之为主线程,或者主isolate)。使用Isolate.spawn可以创建一个新的Isolate。

import 'dart:io';
import 'dart:isolate';

int i;

void main() {
    i = 2;
    Isolate.spawn(entryPoint, "hello");//开启一个子Isolate
}

//entryPoint相当于在一个新的子线程中执行
void entryPoint(Stirng message) {
    print(i); //输出为null,而非2,也说明了主isolate与子isolate内存隔离,无法共享
    sleep(Duration(seconds: 10)); //子isolate延迟,不会影响主isolate
    print(message);
}

    消息接收器和消息发送器即ReceivePort和SendPort,它们在每个isolate中均存在,并且只有同一个isolate中的ReceivePort才能接受到其SendPort发送的消息并进行处理。

void main() {
    //创建一个消息接收器
    var receivePort = new ReceivePort();

    //从消息接收器读取消息
    //消息接收器添加监听,监听接收的消息是发送器发送过来的
    receivePort.listen((t) {
        print(t);
    });

    //发送消息
    receivePort.sendPort.send("1");
    receivePort.sendPort.send("2");
    receivePort.sendPort.send(1);

    //使用完毕关闭消息接收器
    receivePort.close();
}

主isolate和子isolate之间如何通信? -- 通过SendPort和ReceivePort

1、主线程中创建消息接收器

void main() {
    //创建一个消息接收器
    var receivePort = new ReceivePort();

    //开启一个子isolate,并将主isolate消息接收器中的发送器发给子isolate
    Isolate.spawn(entryPoint, receivePort.sendPort);

    //消息接收器添加监听
    receiverPort.listen((t) {
        print(t); //主isolate接收到子isolate中发送的消息
    });
}

void entryPoint(SendPort sendPort) {
    sendPort.send("111"); //在子isolate中使用发送器发送消息
}

2、子线程中创建消息接收器

void entryPoint(SendPort sendPort) {
    var receivePort = new ReceivePort();
    var sendPort2 = receivePort.sendPort; //子isolate的消息发送器
    sendPort.send(sendPort2);

    receivePort.listen((t) { //子isolate接收主isolate发送的消息

    });
}

void main() {
    var receivePort = new ReceivePort();
    Isolate.spawn(entryPoint, receivePort.sendPort);

    receivePort.listen((t) {
        print("接收到其他isolate发送过来的消息!");
        //主isolate中接收到子isolate中的消息发送器
        if(t is SendPort) {
            //to do
        }
    });
}

Dart中每一个isolate都相当于一个独立的执行环境。消息驱动机制存在于Dart的isolate中,每一个isolate中都有一个独立的完整的event-loop任务队列。这种消息驱动机制与Android中的消息驱动机制类似,不同的是,Dart中有两个消息队列,一个是普通事件队列(event queue),另一个是微任务队列(microtask queue)。Dart执行完main函数之后,就会通过loop开始执行两个任务队列中的事件。Loop首先检查微任务队列,依次执行微任务队列中的event;当微任务队列执行完之后,检查event queue队列依次执行,在执行event queue过程中,每执行完一个event就再检查一次微任务队列。故微任务队列的优先级较高,我们可以利用微任务队列进行消息插队。

void main() {
    var receivePort = new ReceivePort();
    receivePort.listen((t) {
        print(t);
    });

    //使用Future.microtask往微任务队列中提交一个消息
    Future.microtask(() {
        print("微任务执行--1");
    });

    receivePort.sendPort.send("发消息给消息接收器--1");

    Future.microtask(() {
        print("微任务执行--2");
    });

    receivePort.sendPort.send("发消息给消息接收器--2");

    Future.microtask(() {
        print("微任务执行--3");
    });

    receivePort.sendPort.send("发消息给消息接收器--3");
}

上述示例代码会先打印微任务执行,再打印普通任务消息,说明微任务队列的优先级较高,输出为

微任务执行--1
微任务执行--2
微任务执行--3
发消息给消息接收器--1
发消息给消息接收器--2
发消息给消息接收器--3

因为Dart是单线程模型,消息队列在同一isolate中。如果微任务队列执行后延迟了几秒,则后续的任务均会延迟执行。

我们平时执行的任务大部分是在event-queue中。Dart中存在插队机制,如果我们希望某个任务优于其他任务执行,可以使用微任务队列进行插队执行。

void main() {
    var receivePort = new ReceivePort();
    receivePort.listen((t) {
        print(t);
        Future.microtask(() {
            print("微任务执行");
        });
    });

    receivePort.sendPort.send("发消息给消息接收器1");
    receivePort.sendPort.send("发消息给消息接收器2");
    receivePort.sendPort.send("发消息给消息接收器3");

    //main函数执行延迟10s,消息队列执行也会延迟10s,也即任务队列是在main函数执行完毕之后执行的
    sleep(Duration(second: 10)); //main函数不是由消息队列执行,此行未加入到消息队列中
}

上述示例代码利用微任务队列实现插队,执行结果为

发消息给消息接收器1
微任务执行
发消息给消息接收器2
微任务执行
发消息给消息接收器3
微任务执行

在Flutter编程中使用更多的是FutureStream

在Java开发中进行并发编程时,会用到Future来获取异步任务执行的结果。在Dart中我们执行一个任务也可以使用Future来完成。Future表示未来执行的任务,是在任务队列中提取任务来执行。Future默认执行的event-queue普通任务队列中的任务,也可以提交任务到微任务队列。Future提供的api中绝大部分是用来执行普通任务的,如:wait(), forEach(), doWhile(), delayed(), any(), error()...,但可以通过Future.microtask()提交一个微任务。

//Future基本使用
void main() {
    Future f = Future.delayed(Duration(seconds: 3)); //将延迟任务放到任务队列中
    f.then((t) { //通过then可以获取future的执行结果
        print(t);
    });
}

对于网络请求或文件操作,java一般将其放在一个子线程中去执行。而在Dart中进行文件IO操作是十分方便的,直接调用对应的异步方法即可。如File相关函数readAsString()、readAsBytes()、readAsLines()等,另外还有对应的同步方readAsStringSync()、readAsBytesSync()、readAsLinesSync()等。例如:

void main() {
    new File(r"c:\test.txt").readAsString()  //读取文件中内容,返回future类对象
                            .then((String s) { //这里获取结果类型确认为String
                                print(s);  //打印读取的字符串
                             })
                            .catchError((s, e) {  //捕获异常
                             });

    Future.delayed(Duration(seconds: 10)); //延迟任务。上一个任务异常会导致后续的任务无法执行
}

then方法返回后可以得到Future任务执行的结果并且返回一个新的Future对象:

void main() {
    Future<int> then = new File(r"c:\test.txt").readAsString().then((String s) {
                                                    print(s); //打印读取的字符串
                                                    return 10; //这里添加返回值
                                                });

    then.then((int i) {
             print(i); //这里打印结果为10
         });
}

或者直接使用链式调用的方式写成如下形式:

void main() {
    Future<int> then = new File(r"c:\test.txt").readAsString().then((String s) {
                                                    print(s); //打印读取的字符串
                                                    return 10;
                                                }).then((int i) {
                                                    print(i); //10
                                                });
}

这里和rxjava中的写法相似,也是返回一个同类型的对象,使用链式调用的方式。 

使用Future.then可以完成有序任务的执行,即前一个任务执行完毕,后一个任务根据前一个任务的结果执行后续的操作。如果要实现一组任务执行完之后再统一执行后续的操作,可以使用Future.wait():

void main() {
    Future.wait([Future.delayed(seconds: 5), 
                 Future.delayed(seconds: 10)]).then((_) {
                                                   print(1);
                                               });
}

Stream:

Stream在Dart中也经常出现,表示发出的一系列异步数据,同样可以用于异步操作。Future表示稍后获得的一个数据,所有异步的操作返回值均用Future表示。它与Future的区别在于,Future表示一次异步获得的数据,Stream表示需要多次异步操作获得的结果。Stream的好处是处理过程中内存占用较小。

void main() {
    var stream = new File(r"c:\test.txt").openRead(); //返回Stream<List<int>>,泛型类型是<List<int>>
    stream.listen((s) {
        print(1); //如果文件较大,会读取多次,但Future会一次性读取完毕
    });
}

Stream.listen()其实就是去订阅这个Stream,它返回一个StreamSubscription类型的订阅者对象。订阅者提供了一些方法方便我们执行订阅的相关操作,如cancel()用于取消监听读取过程,这样listen中就无法接收到任何信息了,onData()可以重置listen方法,onDone()监听读取完毕,pause()和resume()可以停止和恢复监听等。

void main() {
    StreamSubscription<List<int>> listen = new File(r"c:\test.txt").openRead()
                                               .listen((List<int> bytes) {
                                                    print("stream执行");
                                                });
    listen.onData((_) {
        print("替换listen");
    });
    listen.onDone(() {
        print("结束");
    });
    listen.pause(); //暂停,如果没有继续,程序退出
    listen.resume();
}

Stream广播模式:

Stream有两种订阅模式:单订阅模式和多订阅模式。Stream默认是单订阅模式,即只能有一个订阅者,若有多个订阅者会报错。前述使用的都是单订阅模式。而广播模式可以有多个订阅者,通过Stream.asBroadcastStream()可以将一个单订阅模式的stream转换为一个多订阅模式的stream,通过isBroadcast属性可以判断当前stream所属的模式。

void main() {
    var stream = new File(r"c:\test.txt").openRead();
    stream.listen((List<int> bytes) {
                     print("stream执行");
                  });
    //如下会报错,单订阅模式只能有一个订阅者
    //stream.listen((_) {
    //                 print("stream执行2");
    //              });

    //如下会报错 ,单订阅的stream如果已经添加了订阅者,则不能再转换成广播模式了
    //var streamBroadcast = stream.asBroadcastStream();

    var streamBroadcast = new File(r"c:\test.txt").openRead().asBroadcastStream();
    streamBroadcast.listen((_) {
        print("订阅者1");
    });
    streamBroadcast.listen((_) {
        print("订阅者2");
    });
    streamBroadcast.listen((_) {
        print("订阅者3");
    });
}

Stream的广播模式除了通过上述使用单订阅模式转换的方式,还可以使用StreamController直接创建广播:

void main() {
    //创建一个广播,对stream进行管理
    var streamController = StreamController.broadcast();
    //添加一个事件 即发送一条广播
    streamController.add("11");
    //监听
    //先发出事件再订阅,无法收到通知
    streamController.stream.listen((i) {
        print("广播:$i");
    });

    //使用完关闭
    streamController.close();

    //--------------------------------------------
    var stream = Stream.forIterable([1, 2, 3, 4]);
    //单订阅转换的广播消息发出后再监听也是可以收到消息的
    var broadcastStream = stream.asBroadcastStream();
    broadcastStream.listen((i) {
        print("订阅者:${i}");
    });

    new Timer(Duration(seconds: 3), ()=> {
        broadcastStream.listen((i) {
            print("订阅者2:${i}"); //也可以收到消息
        });
    });
}

广播模式下无法接收到订阅前的消息,而通过单订阅模式转换的广播先发送再注册是可以接收到消息的,利用此特性可以实现粘性广播。

StreamController实现了StreamSink,而StreamSink实现了EventSink,EventSink有add,addError,close等方法,EventSink实现了sink,EventSink和Sink均是抽象类,Sink只有add(发出一个广播)和close(关闭)两个方法。

asyncawait

这是Dart中的两个关键字,使用async和await的代码表示是异步执行的。

//async表示这是一个异步方法,异步方法只能返回void和Future
//await必须在async方法中使用
Future<String> readFile() async {
    //await 等待future执行完之后再执行后续的代码,即实现了同步的效果,其作用类似then方法,可以避免then方法回调地狱
    String content = await new File("/path/test.txt").readAsString();
    String content2 = await new File("/path/test.txt").readAsString();
    String content3 = await new File("/path/test.txt").readAsString();
    //自动转换为Future
    return content;
}

void main() {
  new File("/path/test.txt").readAsString(); //这里读取文件不是同步的。
  new File("/path/test.txt").readAsString();
}

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!