网站地图    收藏   

主页 > 前端 > dart入门教程 >

Dart异步编程详解

来源:未知    时间:2023-05-08 18:28 作者:小飞侠 阅读:

[导读] 原创: https://blog.csdn.net/wjw465150?spm=1011.2415.3001.5343 Dart异步编程详解 Future Future表示在将来某时获取一个值的方式。当 一个返回Future的函数被调用 的时候,做了两件事情: 函数把自己放入...

原创: https://blog.csdn.net/wjw465150?spm=1011.2415.3001.5343

Dart异步编程详解


Future

Future表示在将来某时获取一个值的方式。当一个返回Future的函数被调用的时候,做了两件事情:

  1. 函数把自己放入队列和返回一个未完成的Future对象

  2. 之后当值可用时,Future带着值变成完成状态。

为了获得Future的值,有两种方式:

  1. 使用async和await

  2. 使用Future的接口

Future 对象封装了Dart 的异步操作,它有未完成(uncompleted)和已完成(completed)两种状态。

在Dart中,所有涉及到IO的函数都封装成Future对象返回,在你调用一个异步函数的时候,在结果或者错误返回之前,你得到的是一个uncompleted状态的Future。

completed状态也有两种:一种是代表操作成功,返回结果;另一种代表操作失败,返回错误。

我们来看一个例子:


Future<String> fetchUserOrder() {
  //想象这是个耗时的数据库操作
  return Future(() => 'Large Latte');}void main() {
  fetchUserOrder().then((result){print(result)})
  print('Fetching user order...');}

通过then来回调成功结果,main会先于Future里面的操作,输出结果:


Fetching user order...
Large Latte

在上面的例子中,() => 'Large Latte')是一个匿名函数,=> 'Large Latte' 相当于 return 'Large Latte'

Future同名构造器是factory Future(FutureOr<T> computation()),它的函数参数返回值为FutureOr<T>类型,我们发现还有很多Future中的方法比如Future.then、Future.microtask的参数类型也是FutureOr<T>,看来有必要了解一下这个对象。


FutureOr<T>

FutureOr<T> 是个特殊的类型,它没有类成员,不能实例化,也不可以继承,看来它很可能只是一个语法糖


abstract class FutureOr<T> {
  // Private generative constructor, so that it is not subclassable, mixable, or
  // instantiable.
  FutureOr._() {
    throw new UnsupportedError("FutureOr can't be instantiated");
  }}

你可以把它理解为受限制的dynamic类型,因为它只能接受Future或者T类型的值:


FutureOr<int> hello(){}void main(){
   FutureOr<int> a = 1; //OK
   FutureOr<int> b = Future.value(1); //OK
   FutureOr<int> aa = '1' //编译错误

   int c = hello(); //ok
   Future<int> cc = hello(); //ok
   String s = hello(); //编译错误}

在 Dart 的最佳实践里面明确指出:请避免声明函数返回类型为FutureOr<T>

如果调用下面的函数,除非进入源代码,否则无法知道返回值的类型究竟是int 还是Future<int>


FutureOr<int> triple(FutureOr<int> value) async => (await value) * 3;

正确的写法:


Future<int> triple(FutureOr<int> value) async => (await value) * 3;

稍微交代了下FutureOr<T>,我们继续研究Future。

如果Future内的函数执行发生异常,可以通过Future.catchError来处理异常:


Future<void> fetchUserOrder() {
  return Future.delayed(Duration(seconds: 3), () => throw Exception('Logout failed: user ID is invalid'));}void main() {
  fetchUserOrder().catchError((err, s){print(err);});
  print('Fetching user order...');}

输出结果:


Fetching user order...
Exception: Logout failed: user ID is invalid

Future支持链式调用:


Future<String> fetchUserOrder() {
  return Future(() => 'AAA');}void main() {
   fetchUserOrder().then((result) => result + 'BBB')
     .then((result) => result + 'CCC')
     .then((result){print(result);});}

输出结果:


AAABBBCCC


async 和 await

  1. 先调用登录接口;

  2. 根据登录接口返回的token获取用户信息;

  3. 最后把用户信息缓存到本机。

接口定义:


Future<String> login(String name,String password){
  //登录
}
Future<User> fetchUserInfo(String token){
  //获取用户信息
}
Future saveUserInfo(User user){
  // 缓存用户信息
}

用Future大概可以这样写:


login('name','password').then((token) => fetchUserInfo(token))
  .then((user) => saveUserInfo(user));

换成async 和await 则可以这样:


void doLogin() async {
  String token = await login('name','password'); //await 必须在 async 函数体内
  User user = await fetchUserInfo(token);
  await saveUserInfo(user);
}

声明了async 的函数,返回值是必须是Future对象。即便你在async函数里面直接返回T类型数据,编译器会自动帮你包装成Future类型的对象,如果是void函数,则返回Future对象。在遇到await的时候,又会把Futrue类型拆包,把原来的数据类型暴露出来,请注意,await 所在的函数必须添加async关键词

await的代码发生异常,捕获方式跟同步调用函数一样:


void doLogin() async {
  try {
    var token = await login('name','password');
    var user = await fetchUserInfo(token);
    await saveUserInfo(user);
  } catch (err) {
    print('Caught error: $err');
  }
}

得益于async 和await 这对语法糖,你可以用同步编程的思维来处理异步编程,大大简化了异步代码的处理。

注:Dart 中非常多的语法糖,它提高了我们的编程效率,但同时也会让初学者容易感到迷惑。

送多一颗语法糖给你:


Future<String> getUserInfo() async {
  return 'aaa';
}

等价于:

Future<String> getUserInfo() async {
  return Future.value('aaa');
}


Completer

不多BB,用法直接上代码。


void getData<T>() async{
  var completer = Completer();
  Future zyn = completer.future;
  zyn.then((_) => print('运行的Future第一个then'))
                .then((_) => print('运行的Future第二个then'))
                .whenComplete(()=>print('运行whenComplete'))
                .catchError((_)=>print('运行catchError'));
  print('先干点别的');
  completer.complete();}

打印结果如下:


flutter: 先干点别的
flutter: 运行的Future第一个then
flutter: 运行的Future第二个then
flutter: 运行whenComplete

结论:CompleterFuture的控制权在我们自己手里,我们来控制Future的调用。

Completer允许你做某个异步事情的时候,调用c.complete(value)方法来传入最后要返回的值。最后通过c.future的返回值来得到结果,(注意:宣告完成的complete和completeError方法只能调用一次,不然会报错)


Dart异步原理

Dart 是一门单线程编程语言。对于平时用 Java 的同学,首先可能会反应:那如果一个操作耗时特别长,不会一直卡住主线程吗?比如Android,为了不阻塞UI主线程,我们不得不通过另外的线程来发起耗时操作(网络请求/访问本地文件等),然后再通过Handler来和UI线程沟通。Dart 究竟是如何做到的呢?

先给答案:异步 IO + 事件循环。下面具体分析。


I/O 模型

我们先来看看阻塞IO是什么样的:


int count = io.read(buffer); //阻塞等待

注: IO 模型是操作系统层面的,这一小节的代码都是伪代码,只是为了方便理解。

当相应线程调用了read之后,它就会一直在那里等着结果返回,什么也不干,这是阻塞式的IO。

但我们的应用程序经常是要同时处理好几个IO的,即便一个简单的手机App,同时发生的IO可能就有:用户手势(输入),若干网络请求(输入输出),渲染结果到屏幕(输出);更不用说是服务端程序,成百上千个并发请求都是家常便饭。

有人说,这种情况可以使用多线程啊。这确实是个思路,但受制于CPU的实际并发数,每个线程只能同时处理单个IO,性能限制还是很大,而且还要处理不同线程之间的同步问题,程序的复杂度大大增加。

如果进行IO的时候不用阻塞,那情况就不一样了:


while(true){
  for(io in io_array){
      status = io.read(buffer);// 不管有没有数据都立即返回
      if(status == OK){
       
      }
  }}

有了非阻塞IO,通过轮询的方式,我们就可以对多个IO进行同时处理了,但这样也有一个明显的缺点:在大部分情况下,IO都是没有内容的(CPU的速度远高于IO速度),这样就会导致CPU大部分时间在空转,计算资源依然没有很好得到利用。

为了进一步解决这个问题,人们设计了IO多路复用(IO multiplexing),可以对多个IO监听和设置等待时间:


while(true){
    //如果其中一路IO有数据返回,则立即返回;如果一直没有,最多等待不超过timeout时间
    status = select(io_array, timeout); 
    if(status  == OK){
      for(io in io_array){
          io.read() //立即返回,数据都准备好了
      }
    }}

IO 多路转接有多种实现,比如select、poll、epoll等,我们不具体展开。

有了IO多路转接,CPU资源利用效率又有了一个提升。

眼尖的同学可能有发现,在上面的代码中,线程依然是可能会阻塞在 select 上或者产生一些空转的,有没有一个更加完美的方案呢?

答案就是异步IO了:


io.async_read((data) => {
  // dosomething});

通过异步IO,我们就不用不停问操作系统:你们准备好数据了没?而是一有数据系统就会通过消息或者回调的方式传递给我们。这看起来很完美了,但不幸的是,不是所有的操作系统都很好地支持了这个特性,比如Linux的异步IO就存在各种缺陷,所以在具体的异步IO实现上,很多时候可能会折中考虑不同的IO模式,比如 Node.js 的背后的libeio库,实质上采用线程池与阻塞 I/O 模拟出来的异步 I/O。

Dart 在文档中也提到是借鉴了 Node.js 、EventMachine, 和 Twisted 来实现的异步IO,我们暂不深究它的内部实现(笔者在搜索了一下Dart VM的源码,发现在android和linux上似乎是通过epoll实现的),在Dart层,我们只要把IO当做是异步的就行了。

我们再回过头来看看上面Future那段代码:


Future<Response> respFuture = http.get('https://example.com'); //发起请求

现在你知道,这个网络请求不是在主线程完成的,它实际上把这个工作丢给了运行时或者操作系统。这也是 Dart 作为单进程语言,但进行IO操作却不会阻塞主线程的原因。

终于解决了Dart单线程进行IO也不会卡的疑问,但主线程如何和大量异步消息打交道呢?接下来我们继续讨论Dart的事件循环机制(Event Loop)。


事件循环 (Event Loop)

在Dart中,每个线程都运行在一个叫做isolate的独立环境中,它的内存不和其他线程共享,它在不停干一件事情:从事件队列中取出事件并处理它。


while(true){
   event = event_queue.first() //取出事件
   handleEvent(event) //处理事件
   drop(event) //从队列中移除}

比如下面这段代码:


RaisedButton(
  child: Text('click me');
  onPressed: (){ // 点击事件 
     Future<Response> respFuture = http.get('https://example.com'); 
     respFuture.then((response){ // IO 返回事件
        if(response.statusCode == 200){
           print('success');
        }
     })
  })

当你点击屏幕上按钮时,会产生一个事件,这个事件会放入isolate的事件队列中;接着你发起了一个网络请求,也会产生一个事件,依次进入事件循环。

在线程比较空闲的时候,isolate还可以去搞搞垃圾回收(GC),喝杯咖啡什么的。

API层的Future、Stream、async 和 await 实际都是对事件循环在代码层的抽象。结合事件循环,回到对Future对象的定义(An object representing a delayed computation.),就可以这样理解了:isolate大哥,我快递一个代码包裹给你,你拿到后打开这个盒子,并顺序执行里面的代码。

事实上,isolate 里面有两个队列,一个就是事件队列(event queue),还有一个叫做微任务队列(microtask queue)。

事件队列:用来处理外部的事件,如果IO、点击、绘制、计时器(timer)和不同 isolate 之间的消息事件等。

微任务队列:处理来自于Dart内部的任务,适合用来不会特别耗时或紧急的任务,微任务队列的处理优先级比事件队列的高,如果微任务处理比较耗时,会导致事件堆积,应用响应缓慢。

img

你可以通过Future.microtask 来向isolate提交一个微任务:


import 'dart:async';main() {
  new Future(() => print('beautiful'));
  Future.microtask(() => print('hi'));}

输出:


hi
beautiful

总结一下事件循环的运行机制:当应用启动后,它会创建一个isolate,启动事件循环,按照FIFO的顺序,优先处理微任务队列,然后再处理事件队列,如此反复。


多线程

注:以下当我们提到isolate的时候,你可以把它等同于线程,但我们知道它不仅仅是一个线程。

得益于异步 IO + 事件循环,尽管Dart是单线程,一般的IO密集型App应用通常也能获得出色的性能表现。但对于一些计算量巨大的场景,比如图片处理、反序列化、文件压缩这些计算密集型的操作,只单靠一个线程就不够用了。

在Dart中,你可以通过Isolate.spawn 来创建一个新的isolate:


void newIsolate(String mainMessage){
  sleep(Duration(seconds: 3));
  print(mainMessage);}void main() {
  // 创建一个新的isolate,newIoslate
  Isolate.spawn(newIsolate, 'Hello, Im from new isolate!'); 
  sleep(Duration(seconds: 10)); //主线程阻塞等待}

输出:


Hello, Im from new isolate!

spawn 有两个必传参数,第一个是新isolate入口函数(entrypoint),第二个是这个入口函数的参数值(message)。

如果主isolate想接收子isolate的消息,可以在主isolate创建一个ReceivePort对象,并把对应的receivePort.sendPort作为新isolate入口函数参数传入,然后通过ReceivePort绑定SendPort对象给主isolate发送消息:

最新评论

添加评论

自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习

京ICP备14009008号-1@版权所有www.zixuephp.com

网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com

添加评论