Flutter中的异步

移动端团队 @ 奇舞团(360集团大前端团队)

同步与异步

程序的运行是出于满足人们对某种逻辑需求的处理,在计算机上表现为可执行指令,正常情况下我们期望的指令是按逻辑的顺序依次执行的,而实际情况由于某些指令是耗时操作,不能立即返回结果而造成了阻塞,导致程序无法继续执行。这种情况多见于一些io操作。这时,对于用户层面来说,我们可以选择stop the world,等待操作完成返回结果后再继续操作,也可以选择继续去执行其他操作,等事件返回结果后再通知回来。这就是从用户角度来看的同步与异步。

从操作系统的角度,同步异步,与任务调度,进程间切换,中断,系统调用之间有着更为复杂的关系。

同步I/O 与 异步I/O的区别

img

为什么使用异步

用户可以阻塞式的等待,因为人的操作和计算机相比是非常慢的,计算机如果阻塞那就是很大的性能浪费了,异步操作让您的程序在等待另一个操作的同时完成工作。三种异步操作的场景:

  • I/O操作:例如:发起一个网络请求,读写数据库、读写文件、打印文档等,一个同步的程序去执行这些操作,将导致程序的停止,直到操作完成。更有效的程序会改为在操作挂起时去执行其他操作,假设您有一个程序读取一些用户输入,进行一些计算,然后通过电子邮件发送结果。发送电子邮件时,您必须向网络发送一些数据,然后等待接收服务器响应。等待服务器响应所投入的时间是浪费的时间,如果程序继续计算,这将得到更好的利用
  • 并行执行多个操作:当您需要并行执行不同的操作时,例如进行数据库调用、Web 服务调用以及任何计算,那么我们可以使用异步
  • 长时间运行的基于事件驱动的请求:这就是您有一个请求进来的想法,并且该请求进入休眠状态一段时间等待其他一些事件的发生。当该事件发生时,您希望请求继续,然后向客户端发送响应。所以在这种情况下,当请求进来时,线程被分配给该请求,当请求进入睡眠状态时,线程被发送回线程池,当任务完成时,它生成事件并从线程池中选择一个线程发送响应

计算机中异步的实现方式就是任务调度,也就是进程的切换

任务调度采用的是时间片轮转的抢占式调度方式,进程是任务调度的最小单位。

计算机系统分为用户空间内核空间,用户进程在用户空间,操作系统运行在内核空间,内核空间的数据访问修改拥有高于普通进程的权限,用户进程之间相互独立,内存不共享,保证操作系统的运行安全。如何最大化的利用CPU,确定某一时刻哪个进程拥有CPU资源就是任务调度的过程。内核负责调度管理用户进程,以下为进程调度过程

img

在任意时刻, 一个 CPU 核心上(processor)只可能运行一个进程

每一个进程可以包含多个线程,线程是执行操作的最小单元,因此进程的切换落实到具体细节就是正在执行线程的切换

Future

Future<T> 表示一个异步的操作结果,用来表示一个延迟的计算,返回一个结果或者error,使用代码实例:

Future<int> future = getFuture();
future.then((value) => handleValue(value))
      .catchError((error) => handleError(error))
  		.whenComplete(func);
复制代码

future可以是三种状态:未完成的返回结果值返回异常

当一个返回future对象被调用时,会发生两件事:

  • 将函数操作入队列等待执行结果并返回一个未完成的Future对象
  • 函数操作完成时,Future对象变为完成并携带一个值或一个错误

首先,Flutter事件处理模型为先执行main函数,完成后检查执行微任务队列Microtask Queue中事件,最后执行事件队列Event Queue中的事件,示例:

void main(){
  Future(() => print(10));
	Future.microtask(() => print(9));
  print("main");
}
/// 打印结果为:
/// main
/// 9
/// 10
复制代码

基于以上事件模型的基础上,看下Future提供的几种构造函数,其中最基本的为直接传入一个Function

factory Future(FutureOr<T> computation()) {
    _Future<T> result = new _Future<T>();
    Timer.run(() {
      try {
        result._complete(computation());
      } catch (e, s) {
        _completeWithErrorCallback(result, e, s);
      }
    });
    return result;
  }
复制代码

Function有多种写法:

//简单操作,单步
Future(() => print(5));
//稍复杂,匿名函数
Future((){
  print(6);
});
//更多操作,方法名
Future(printSeven);

printSeven(){
  print(7);
}
  
复制代码

Future.microtask

此工程方法创建的事件将发送到微任务队列Microtask Queue,具有相比事件队列Event Queue优先执行的特点

factory Future.microtask(FutureOr<T> computation()) {
    _Future<T> result = new _Future<T>();
  	//
    scheduleMicrotask(() {
      try {
        result._complete(computation());
      } catch (e, s) {
        _completeWithErrorCallback(result, e, s);
      }
    });
    return result;
  }
复制代码

Future.sync

返回一个立即执行传入参数的Future,可理解为同步调用

factory Future.sync(FutureOr<T> computation()) {
    try {
      var result = computation();
      if (result is Future<T>) {
        return result;
      } else {
        // TODO(40014): Remove cast when type promotion works.
        return new _Future<T>.value(result as dynamic);
      }
    } catch (error, stackTrace) {
      /// ...
    }
  }
复制代码
	Future.microtask(() => print(9));
  Future(() => print(10));
  Future.sync(() => print(11));

 	/// 打印结果: 11、9、10
复制代码

Future.value

创建一个将来包含value的future

factory Future.value([FutureOr<T>? value]) {
    return new _Future<T>.immediate(value == null ? value as T : value);
  }
复制代码

参数FutureOr含义为T value 和 Future value 的合集,因为对于一个Future参数来说,他的结果可能为value或者是Future,所以对于以下两种写法均合法:

	Future.value(12).then((value) => print(value));
  Future.value(Future<int>((){
    return 13;
  }));
复制代码

这里需要注意即使value接收的是12,仍然会将事件发送到Event队列等待执行,但是相对其他Future事件执行顺序会提前

Future.error

创建一个执行结果为error的future

factory Future.error(Object error, [StackTrace? stackTrace]) {
    /// ...
    return new _Future<T>.immediateError(error, stackTrace);
  }

_Future.immediateError(var error, StackTrace stackTrace)
      : _zone = Zone._current {
    _asyncCompleteError(error, stackTrace);
  }
复制代码
 Future.error(new Exception("err msg"))
    .then((value) => print("err value: $value"))
    .catchError((e) => print(e));

/// 执行结果为:Exception: err msg
复制代码

Future.delayed

创建一个延迟执行回调的future,内部实现为Timer加延时执行一个Future

factory Future.delayed(Duration duration, [FutureOr<T> computation()?]) {
    /// ...
    new Timer(duration, () {
      if (computation == null) {
        result._complete(null as T);
      } else {
        try {
          result._complete(computation());
        } catch (e, s) {
          _completeWithErrorCallback(result, e, s);
        }
      }
    });
    return result;
  }
复制代码

Future.wait

等待多个Future并收集返回结果

static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
      {bool eagerError = false, void cleanUp(T successValue)?}) {
      /// ...
    }
复制代码

FutureBuilder结合使用:

child: FutureBuilder(
          future: Future.wait([
            firstFuture(),
            secondFuture()
          ]),
          builder: (context,snapshot){
            if(!snapshot.hasData){
              return CircularProgressIndicator();
            }
            final first = snapshot.data[0];
            final second = snapshot.data[1];
            return Text("data $first $second");
          },
        ),
复制代码

Future.any

返回futures集合中第一个返回结果的值

static Future<T> any<T>(Iterable<Future<T>> futures) {
    var completer = new Completer<T>.sync();
    void onValue(T value) {
      if (!completer.isCompleted) completer.complete(value);
    }
    void onError(Object error, StackTrace stack) {
      if (!completer.isCompleted) completer.completeError(error, stack);
    }
    for (var future in futures) {
      future.then(onValue, onError: onError);
    }
    return completer.future;
  }
复制代码

对上述例子来说,Future.any snapshot.data 将返回firstFuturesecondFuture中第一个返回结果的值

Future.forEach

为传入的每一个元素,顺序执行一个action

static Future forEach<T>(Iterable<T> elements, FutureOr action(T element)) {
    var iterator = elements.iterator;
    return doWhile(() {
      if (!iterator.moveNext()) return false;
      var result = action(iterator.current);
      if (result is Future) return result.then(_kTrue);
      return true;
    });
  }
复制代码

这里边action是方法作为参数,头一次见这种形式语法还是在js中,当时就迷惑了很大一会儿,使用示例:

Future.forEach(["one","two","three"], (element) {
    print(element);
  });
复制代码

Future.doWhile

执行一个操作直到返回false

Future.doWhile((){
    for(var i=0;i<5;i++){
      print("i => $i");
      if(i >= 3){
        return false;
      }
    }
    return true;
  });
/// 结果打印到 3
复制代码

以上为Future中常用构造函数和方法

在Widget中使用Future

Flutter提供了配合Future显示的组件FutureBuilder,使用也很简单,伪代码如下:

child: FutureBuilder(
	future: getFuture(),
	builder: (context, snapshot){
		if(!snapshot.hasData){
			return CircularProgressIndicator();
		} else if(snapshot.hasError){
			return _ErrorWidget("Error: ${snapshot.error}");
		} else {
			return _ContentWidget("Result: ${snapshot.data}")
		}
	}
)
复制代码

Async-await

使用

这两个关键字提供了异步方法的同步书写方式,Future提供了方便的链式调用使用方式,但是不太直观,而且大量的回调嵌套造成可阅读性差。因此,现在很多语言都引入了await-async语法,学习他们的使用方式是很有必要的。

两条基本原则:

  • 定义一个异步方法,必须在方法体前声明 async
  • await关键字必须在async方法中使用

首先,在要执行耗时操作的方法体前增加async:

void main() async { ··· }
复制代码

然后,根据方法的返回类型添加Future修饰

Future<void> main() async { ··· }
复制代码

现在就可以使用await关键字来等待这个future执行完毕

print(await createOrderMessage());
复制代码

例如实现一个由一级分类获取二级分类,二级分类获取详情的需求,使用链式调用的代码如下:

var list = getCategoryList();
  list.then((value) => value[0].getCategorySubList(value[0].id))
      .then((subCategoryList){
        var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
        print(courseList);
  }).catchError((e) => (){
    print(e);
  });
复制代码

现在来看下使用async/await,事情变得简单了多少

Future<void> main() async {
  await getCourses().catchError((e){
    print(e);
  });
}
Future<void> getCourses() async {
  var list = await getCategoryList();
  var subCategoryList = await list[0].getCategorySubList(list[0].id);
  var courseList = subCategoryList[0].getCourseListByCategoryId(subCategoryList[0].id);
  print(courseList);
}
复制代码

可以看到这样更加直观

缺陷

async/await 非常方便,但是还是有一些缺点需要注意

因为它的代码看起来是同步的,所以是会阻塞后面的代码执行,直到await返回结果,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但后边自己的代码被阻塞。

这意味着代码可能会由于有大量await代码相继执行而阻塞,本来用Future编写表示并行的操作,现在使用await变成了串行,例如,首页有一个同时获取轮播接口,tab列表接口,msg列表接口的需求

Future<String> getBannerList() async {
  return await Future.delayed(Duration(seconds: 3),(){
    return "banner list";
  });
}

Future<String> getHomeTabList() async {
  return await Future.delayed(Duration(seconds: 3),(){
    return "tab list";
  });
}

Future<String> getHomeMsgList() async {
  return await Future.delayed(Duration(seconds: 3),(){
    return "msg list";
  });
}
复制代码

使用await编写很可能会写成这样,打印执行操作的时间

Future<void> main2() async {
  var startTime = DateTime.now().second;
  await getBannerList();
  await getHomeTabList();
  await getHomeMsgList();
  var endTime = DateTime.now().second;
  print(endTime - startTime); // 9
}
复制代码

在这里,我们直接等待所有三个模拟接口的调用,使每个调用3s。后续的每一个都被迫等到上一个完成, 最后会看到总运行时间为9s,而实际我们想三个请求同时执行,代码可以改成如下这种:

Future<void> main() async {
  var startTime = DateTime.now().second;
  var bannerList = getBannerList();
  var homeTabList = getHomeTabList();
  var homeMsgList = getHomeMsgList();

  await bannerList;
  await homeTabList;
  await homeMsgList;
  var endTime = DateTime.now().second;
  print(endTime - startTime); // 3
}
复制代码

将三个Future存储在变量中,这样可以同时启动,最后打印时间仅为3s,所以在编写代码时,我们必须牢记这点,避免性能损耗。

原理

线程模型

当一个Flutter应用或者Flutter Engine启动时,它会启动(或者从池中选择)另外三个线程,这些线程有些时候会有重合的工作点,但是通常,它们被称为UI线程GPU线程IO线程。需要注意一点这个UI线程并不是程序运行的主线程,或者说和其他平台上的主线程理解不同,通常的,Flutter将平台的主线程叫做"Platform thread"

img

UI线程是所有的Dard代码运行的地方,例如framework和你的应用,除非你启动自己的isolates,否则Dart将永远不会运行在其他线程。平台线程是所有依赖插件的代码运行的地方。该线程也是native frameworks为其他任务提供服务的地方,一般来说,一个Flutter应用启动的时候会创建一个Engine实例,Engine创建的时候会创建一个Platform thread为其提供服务。跟Flutter Engine的所有交互(接口调用)必须发生在Platform Thread,试图在其它线程中调用Flutter Engine会导致无法预期的异常。这跟Android/iOS UI相关的操作都必须在主线程进行相类似。

Isolates是Dart中概念,本意是隔离,它的实现功能和thread类似,但是他们之间的实现又有着本质的区别,Isolote是独立的工作者,它们之间不共享内存,而是通过channel传递消息。Dart是单线程执行代码,Isolate提供了Dart应用可以更好的利用多核硬件的解决方案。

事件循环

单线程模型中主要就是在维护着一个事件循环(Event Loop) 与 两个队列(event queue和microtask queue)当Flutter项目程序触发如点击事件IO事件网络事件时,它们就会被加入到eventLoop中,eventLoop一直在循环之中,当主线程发现事件队列不为空时发现,就会取出事件,并且执行。

microtask queue中事件优先于event queue执行,当有任务发送到microtask队列时,会在当前event执行完成后,阻塞当前event queue转而去执行microtask queue中的事件,这样为Dart提供了任务插队的解决方案。

event queue的阻塞意味着app无法进行UI绘制,响应鼠标和I/O等事件,所以要谨慎使用,如下为流程图:

event queue和microtask queue

这两个任务队列中的任务切换在某些方面就相当于是协程调度机制

协程

协程是一种协作式的任务调度机制,区别于操作系统的抢占式任务调度机制,它是用户态下面的,避免线程切换的内核态、用户态转换的性能开销。它让调用者自己来决定什么时候让出cpu,比操作系统的抢占式调度所需要的时间代价要小很多,后者为了恢复现场会保存相当多的状态(不仅包括进程上下文的虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态),并且会频繁的切换,以现在流行的大多数Linux机器来说,每一次的上下文切换要消耗大约1.2-1.5μs的时间,这是仅考虑直接成本,固定在单个核心以避免迁移的成本,未固定情况下,切换时间可达2.2μs

img

对cpu来说这算一个很长的时间吗,一个很好的比较是memcpy,在相同的机器上,完成一个64KiB数据的拷贝需要3μs的时间,上下文的切换比这个操作稍微快一些

Plot of thread/process launch and context switch

协程和线程非常相似,是从异步执行任务的角度来看,而并不是从设计的实体角度像进程->线程->协程这样类似于细胞->原子核->质子中子这样的关系。可以理解为线程上执行的一段函数,用yield完成异步请求、注册回调/通知器、保存状态,挂起控制流、收到回调/通知、恢复状态、恢复控制流的所有过程

多线程执行任务模型如图:

线程的阻塞要靠系统间进程的切换,完成逻辑流的执行,频繁的切换耗费大量资源,而且逻辑流的执行数量严重依赖于程序申请到的线程的数量。

协程是协同多任务的,这意味着协程提供并发性但不提供并行性,执行流模型图如下:

协程可以用逻辑流的顺序去写控制流,协程的等待会主动释放cpu,避免了线程切换之间的等待时间,有更好的性能,逻辑流的代码编写和理解上也简单的很多

但是线程并不是一无是处,抢占式线程调度器事实上提供了准实时的体验。例如Timer,虽然不能确保在时间到达的时候一定能够分到时间片运行,但不会像协程一样万一没有人让出时间片就永远得不到运行……

总结

  • 同步与异步
  • Future提供了Flutter中异步代码链式编写方式
  • async-wait提供了异步代码的同步书写方式
  • Future的常用方法和FutureBuilder编写UI
  • Flutter中线程模型,四个线程
  • 单线程语言的事件驱动模型
  • 进程间切换和协程对比

参考

dart.cn/tutorials/l…

dart.cn/codelabs/as…

medium.com/dartlang/da…

juejin.cn/post/684490…

developer.mozilla.org/en-US/docs/…

www.zhihu.com/question/19…

www.zhihu.com/question/50…

en.wikipedia.org/wiki/Asynch…

eli.thegreenplace.net/2018/measur…

文章分类
Android
文章标签