Dart 异步与多线程

6,779 阅读9分钟

Dart单线程

Dart中的事件循环是单线程的,在流畅性与安全性体验较好,核心分为主线程、微任务、宏任务。主线程主要包括业务处理、网络IO、本地文件IO、异步等事件。dart的单线程中有两个事件队列,一个是微任务队列、一个是事件队列。

  • 微任务队列

微任务队列包含有 Dart 内部的微任务,主要是通过 scheduleMicrotask 来调度。

  • 事件队列

事件队列包含外部事件,例如 I/O 、 Timer ,绘制事件等等。

事件循环 Event loop

链式指定事件顺序

如果你的代码具有依赖型,最好是显式的。这有助于开发人员理解您的代码并使您的代码更加健壮。

想给事件队列中链式添加任务的错误操作示范:

future.then(...set an important variable...);
Timer.run(() {...use the important variable...});

正确的是:

future.then(...set an important variable...)
  .then((_) {...use the important variable...});

Dart单线程中,他们的结果都是一致的,但是理解起来难易程度不是一致的。

如何创建任务

当你想稍后在任务中执行你的代码,可以使用Future类,它将把任务添加到事件队列末尾,或者使用顶级函数scheduleMicrotask把任务添加到微任务队列的末尾。

为了更好的使用then,或者在发生错误时也需要执行的话,那么请在whenComplate()代替then.

如何添加任务到事件队列

可以使用Timer或者Future都可以

您也可以使用Timer计划任务,但是如果任务中发生任何未捕获的异常,则应用程序将退出。相反,我们建议使用Future,它建立在Timer之上,并增加了诸如检测任务完成和对错误进行响应的功能。

添加代码到事件队列

new Future(() {
  // ...code goes here...
});

你可以使用then或者whenComplate执行后边的任务,看下这个例子

new Future(() => 21)
    .then((v) => v*2)
    .then((v) => print(v));

如果你想稍后再执行的话,请使用Future.delayed():

new Future.delayed(const Duration(seconds:1), () {
  // ...code goes here...
});

了解了基本的用法之后,那么如何把他们搭配使用呢?

使用任务

有了单线程和队列,那必然有对应的循环,这样子才能执行不同的队列任务和处理事件,那么我们看下 循环。

  1. 进入main函数,并产生相应的微任务和事件队列
  2. 判断是否存在微任务,有则执行,没有则继续。执行完判断是否还有微任务,有则执行,没则继续
  3. 如果不存在可执行的微任务,则判断 是否有事件任务,有则执行,无则继续返回判断是否存在事件任务
  4. 在微任务和事件任务同样可以产生新的 微任务和事件任务,所以需要再次判断是否存在新的微任务和事件任务。

验证一下上边的运行原理,我们看下下边的代码:

void main() {
  test();
}

/// 微任务
/// 定时器
void test() async {
  print('start');
  scheduleMicrotask(() {
    print('Microtask 1');
  });
  Future.delayed(Duration(seconds: 0)).then((value) {
    print('Future 1');
  });
  Timer.run(() {
    print('Timer 1');
  });
print('end');
}

运行过程如下:

  1. 首先启动main函数,打印start
  2. 执行scheduleMicrotask微任务,添加任务到微任务队列中
  3. 执行Future事件,给事件队列添加任务
  4. 执行timer事件,给事件队列添加任务
  5. 执行事件打印end
  6. 第二次循环判断是否有微任务,刚才已添加微任务,现在执行微任务,打印Microtask 1
  7. 判断是否有事件任务,刚才已添加Future任务,执行打印Future 1
  8. 判断是否有事件任务,刚才已添加Timer 1任务,执行打印Timer1

输出

start
end
Microtask 1
Future 1
Timer 1

看下面这个例子证明TimerFuture是一个类型的事件。

/// 微任务
/// 定时器
void test2() async {
  print('start');
  scheduleMicrotask(() {
    print('Microtask 1');
  });
   Timer.run(() {
    print('Timer 1');
  });
  Future.delayed(Duration(seconds: 0)).then((value) {
    print('Future 1');
  });
 

  print('end');
}

输出

start
end
Microtask 1
Timer 1
Future 1

上面的testtest2TimerFuture位置调换了一下,也就是添加事件任务先后顺序颠倒了一下,在执行的时候也颠倒了一下。我们再看Future源码:

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

Future.delayed源码本质就是将任务添加到Timer中,在指定时间后运行该任务。

执行完事件需要再次判断是否有微任务

微任务队列优先级高于事件队列,所以每次执行任务首先判断是否存在未执行的微任务。

事件队列执行完,有可能有新的微任务被添加到队列中,所以还需要扫描微任务队列一次。 看下面的例子:

void test3() async {
  print('start'); //1
  scheduleMicrotask(() {//2
    print('Microtask 1');//5
  });
   Timer.run(() {//3
    print('Timer 1');//6
    Timer.run(() {//9
    print('Timer 1 Microtask 2 ');//10
    });
    scheduleMicrotask(() {//7
        print('Microtask 2');//8
     });
  });
  print('end');//4
}

执行顺序:

  1. 打印 start
  2. 添加微任务到队列中
  3. 添加Timer 1到事件队列
  4. 执行打印 end任务
  5. 判断是否有微任务,发现微任务Microtask 1,立即执行
  6. 判断是有有微任务,发现没有,则判断是否有事件任务,发现Timer 1任务并执行,添加事件任务Timer 1 Microtask 2,添加微任务Microtask 2到微任务队列
  7. 判断是否有微任务,发现微任务Microtask 2,并执行。
  8. 判断是否有事件任务,发现事件任务Timer 1 Microtask 2并执行。

结果输出

start
end
Microtask 1
Timer 1
Microtask 2
Timer 1 Microtask 2

微任务或者事件任务会卡吗?

根据前边的了解,执行完微任务再执行事件任务,当某个事件处理时间需要很长,则后边的任务则会一直处于等待状态。下边我们看一个例子,当任务足够都,还是需要一定时间去处理的。

void test4() async {
  
  print('start ${DateTime.now()}');
  for(int i =0;i < 99999;i++){
    scheduleMicrotask(() {
      print('Microtask 1');
    });
  }
   Timer.run(() {
    print('Timer 1 ${DateTime.now()}');  
  });
  print('end ${DateTime.now()}');
}

输出:

start 2020-07-28 17:44:11.561886
end 2020-07-28 17:44:11.593989
...
Microtask 1
.....

Timer 1 2020-07-28 17:44:11.893093

可以看出这些任务执行完成耗时基本达到了0.33秒。

当一个线程出现处理任务不够了,那么就需要在开启一个线程了。

Isolates 多线程

上边的 dart进入main函数是单线程的,在Dart中,多线程叫做Isolates线程,每个Isolates线程不共享内存,通过消息机制通信

我们看个例子,利用DartIsolates实现多线程。

void test5()async{
  final rece = ReceivePort();
  isolate = await Isolate.spawn(sendPort, rece.sendPort);
   rece.listen((data){
     print('收到了 ${data} ,name:$name');
   });
}
void sendPort(SendPort sendPort){
  sendPort.send('发送消息');
}

Isolate isolate;
String name='fgyong';
void main() {
  test5();
}

输出

收到了 发送消息 ,name:fgyong

多线程相互沟通怎么处理?

创建线程之后子线程需要发送主线程一个端口和消息,主线程记录该端口,下次和子线程通讯使用该端口即可。

具体代码如下:


/// 新线程执行新的任务 并监听
Isolate isolate;
Isolate isolate2;

void createTask() async {
  ReceivePort receivePort = ReceivePort();
  isolate = await Isolate.spawn(sendP1, receivePort.sendPort);
  receivePort.listen((data) {
    print(data);
    if (data is List) {
      SendPort subSencPort = (data as List)[1];
      String msg = (data as List)[0];
      print('$msg 在主线程收到');
      if (msg == 'close') {
        receivePort.close();
      } else if (msg == 'task') {
        taskMain();
      }
      subSencPort.send(['主线程发出']);
    }
  });
}

void sendP1(SendPort sendPort) async {
  ReceivePort receivePort = new ReceivePort();
  receivePort.listen((data) async {
    print(data);
    if (data is List) {
      String msg = (data as List)[0];
      print('$msg 在子线程收到');
      if (msg == 'close') {
        receivePort.close();
      } else if (msg == 'task') {
        var m = await task();
        sendPort.send(['$m', receivePort.sendPort]);
      }
    }
  });
  sendPort.send(['子线程线程发出', receivePort.sendPort]);
}

Future<String> task() async {
  print('子线程执行task');
  for (var i = 0; i < 99999999; i++) {}
  return 'task 完成';
}

void taskMain() {
  print('主线程执行task');
}

输出:

[子线程线程发出, SendPort]
子线程线程发出 在主线程收到
[主线程发出]
主线程发出 在子线程收到

更多子线程与主线程交互请上代码库查看

复杂问题解决方案

假设一个项目,需要 2 个团队去完成,团队中包含多项任务。可以分为 2 个高优先级任务(高优先级的其中,会产生2个任务,一个是紧急一个是不紧急),和 2 个非高优先级任务(非高优先级的其中,会产生有 2 个任务,一个是紧急一个是不紧急)。其中还有一个是必须依赖其他团队去做的,因为本团队没有那方面的资源,第三方也会产生一个高优先级任务和一个低优先级任务。

根据紧急任务作为微任务,非紧急任务作为事件任务来安排,第三方是新开线程

主任务 高优先级(微任务) 低优先级(事件任务) 第三方(Isolate)
H1 H1-1 L1-2
H2 H2-1 L2-2
L3 H3-1 L3-2
L4 H4-1 L4-2
I5 IH5-1 I5-2
void test6() {
  createTask();//创建线程
  scheduleMicrotask(() {//第一个微任务
    print('H1');
    scheduleMicrotask(() {//第一个紧急任务
      print('H1-1');
    });
    Timer.run(() {//第一个非紧急任务
      print('L1-2');
    });
  });
  scheduleMicrotask(() {// 第二个高优先级任务
    print('H2');
    scheduleMicrotask(() {//第二个紧急任务
      print('H2-1');
    });
    Timer.run(() {//第二个非紧急任务
      print('L2-2');
    });
  });
  Timer.run(() {// 第一个低优先级任务
    print('L3');
    scheduleMicrotask(() {//第三个紧急任务
      print('H3-1');
    });
    Timer.run(() {//第三个非紧急任务
      print('L3-2');
    });
  });

  Timer.run(() {// 第二个低优先级任务
    print('L4');
    scheduleMicrotask(() {//第四个紧急任务
      print('H4-1');
    });
    Timer.run(() {//第四个非紧急任务
      print('L4-2');
    });
  });
}

/// 新线程执行新的任务 并监听
Isolate isolate;
void createTask() async {
  ReceivePort receivePort = ReceivePort();
  isolate = await Isolate.spawn(sendPort, receivePort.sendPort);
  receivePort.listen((data) {
    print(data);
  });
}
/// 新线程执行任务 
void sendPort(SendPort sendPort) {

  scheduleMicrotask(() {
    print('IH5-1');
  });
  Timer.run(() {
    print('IL5-2');
  });
  sendPort.send('第三方执行任务结束');
}

运行结果

H1
H2
H1-1
H2-1
L3
H3-1
L4
H4-1
L1-2
L2-2
L3-2
L4-2
IH5-1
IL5-2
第三方执行任务结束

可以看到H开头的为高优先级,L开头为低优先级,基本高优先级运行都在低优先级之前,符合预期。

但是第三方的为什么在最后才执行了?

由于创建线程需要事件,其他任务均为耗时太短,那么我们重新做一个耗时事件长的任务即可。

 createTask();//创建线程
  for (var i = 0; i < 9999999999; i++) {
    
  }
  ...

输出:

IH5-1
IL5-2
H1
H2
H1-1
H2-1
第三方执行任务结束
L3
H3-1
L4
H4-1
L1-2
L2-2
L3-2
L4-2

为什么 第三方执行任务结束 在正中间?而不是在H1上边??

因为这个事件属于低优先级,而H开头的都是高优先级任务。

为什么 第三方执行任务结束 ??

L3上边。而不是在最下边,一定在低优先级队列的第一个吗 ?

由于开始的耗时操作事件太长,导致所有任务执行前,第三方任务已经执行完成,所以 第三方执行任务结束 是第一个添加到低优先级任务队列的,所以在低优先级队列第一个执行。

当耗时操作比较少时,则 第三方执行任务结束 添加顺序则不确定。

总结

Dart中异步和多线程是分开的,异步只是事件循环中的多事件轮训的结果,而多线程可以理解为真正的并发(多个线程同时做事)。在单个线程中,又分为微任务和其他事件队列,微任务队列优先级高于其他事件队列。

参考