Dart中的线程Promise简述

简单原理

  • 因为Dart并不是C语言, 所以对于线程来说, 它并不提供 用户级别的直接系统调用(线程库)

    如C系列的语言(C, C++, OC等), 完全兼容C语言, 而内核又是由C语言编写, 内核提供了线程的C函数调用. <font color = green> 所以 当我们写 C程序时, 可以直接 系统调用内核的线程函数</font>


  • 其他的语言要想实现线程调用, 则对应的编译器要提供 语法调用接口, 由编译器内部转化为系统调用

    如 跨平台的 Java, 线程相关的的语法是Java语法, 但事实上 编译器 会将这些代码对接到系统调用, 也就是常说的 ABI




    再如 js, js引擎(也就是编译器), 也提供了 线程相关的调用, 用户写的js异步操作, 内部会被js引擎 对接当前系统的 ABI




    再如 swift, 也是一样的, 总之只要不是C系列的语言, 就不能由用户直接系统调用, 都要由编译器来完成


  • 这里的Dart也是一样的道理, Dart是由google编写, 事实上Dart编译器 基本是对JS引擎的作了 很大优化, 这一点从语法结构来看2者有很大相似的地方

  • 同样的 Dart的异步也是同JS一样, 有<font color = red>promise->async--await</font> 机制, 只不过名字不同, 语法稍微不同

    ps: 从中可以看到 JS并没有文件IO相关的操作, 因为 JS编译器没有提供文件IO相关的 调用语法接口

  • 还有一点要说, 就是关于网上说的

    • JS是单线程
    • Dart是单线程
  • 这里直接说结论:

  • Dart运行 main.dart程序时, 事实上至少2个线程

    1. Dart运行环境所在的线程
    2. 代码main函数所在的线程就是<font color = red>所谓的主线程</font>
  • 所谓的异步不管在JS或Dart中处理机制都是:将任务添加到main线程所在的任何队列, 等当前主线程所有的任务执行完后,再从队列中取出任务继续执行. 有一点可以验证:<font color = red>Dart环境和main函数所在线程不一样</font>

#include<unistd.h>
#include<iostream>
#include<pthread.h>

void* sub_thread(void* args){
    sleep(1);
    printf("sub_thread\n");
    return nullptr;
}

int main(int args, char** argv){
    printf("main_start\n");
    pthread_t p;
    pthread_create(&p, nullptr, sub_thread, nullptr);

    printf("main_end\n");
    return 0;
}

上述程序可以验证main函数结束后, 整个进程就结束了, 所以所谓的子线程不会有任何输出, 同样的处理逻辑, 放在Dart中如下:

import 'dart:io';

void main(List<String> args) {
  print("start");
  Future(
    () {
      sleep(Duration(seconds: 1));
      print("sub");
    },
  );
  sleep(Duration(secods:5));
  print("end");
}

这里 <font color = red>Future</font>指定了一个回调, 事实上Dart会将回调中的代码在主线程中执行, 最后打印的顺序是:

start       
end             # 5秒后输出
sub             # 再等待1秒输出 

exited ...      # 最后结束 

从中可以看出, Dart在执行Future时, 将任务添加到了主线程的任务队列, 然后等待5秒直接输出end, 然后并未结束, 而再等待1秒, 再输出sub

上述Dart程序和C++唯一不同的是, C++中直接开启了线程, 而当前的Dart程序中, 用户无法开启新的线程, Future的回调会被编译器编译成, 直接在main函数的任务队列中添加该操作


所谓的Dart单线程和异步

  • 其实指的是: <font color = red>编写的任何Future定时器的回调, 其实都是编译器将这个回调的代码添加到了主线程的任务队列, 所以从用户使用的这一角度来看, Dart就是单线程, 添加任务的过程并不是耗时操作. 只有当当前的任务完毕后, 再回头来处理刚添加的任务, 所以就这是所谓的异步, 即不阻塞当前线程</font>

缺点: 任务本质还是在主线程执行, 若任务是耗时的, 则也会阻塞主线程, 如下现在案例:

import 'dart:io';

void main(List<String> args) {
  print("start");
  Future(() {
    sleep(Duration(seconds: 6));
    print("sub2");
  });

  Future.delayed(Duration(seconds: 3), () {
    print("main exec");
    return "hello";
  });

  print("end");
}

该程序目的: 异步执行一个耗时的操作Future, 但同时希望3秒后延迟执行代码delayed, 最终确是:

start
end               # 等待6秒后, 立即输出 sub2
sub2        
main exec         # 输出sub2时, 不行再等待3秒, 而是立即输出

### 究其原因是: 主线程:
##   1. 打印 start
##   2. 添加 睡眠6秒的 任务到 主线程的任务队列中
##   3. 添加 延迟3秒的 任务到 主线程的任务队列中
##   4. 打印 end
###   取出队列的任务 (6秒的睡眠任务), 并在主线程执行, 这6秒中主线程被阻塞
###   被阻塞的过程中, 即使3秒后了, 也不会执行延迟任务
###  5. 6秒时间到 在主线程打印 sub2
###  6. 继续取出队列中的任务, 是一个延迟任务, 此时距离第3步的添加 延迟任务 已经过去了6秒, Dart的处理是, 当前时间已经过去了3秒, 所以不再等待, 直接执行

上述案例中, 若延迟任务是7秒, 则在第6步后, Dart会先看第3步的时任务被添加的时间, 然后再等待1秒后, 执行延迟任务


所谓的Future

简述

  • 对应JS中的 <font color = green size = 4>Promise</font>, 也对应 <font color = red size = 4>C++标准线程库中的Future</font>, 它们在原理上是一样的
  • 在Dart中Future的本质是将任务添加到主线程的任务队列, 然后等待当前所有未执行完毕的任务, 所以现象就是所谓的异步(不阻塞main当前正在处理的任务)

基本使用

  • 结构
abstract interface class Future<T> {
  ...

  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;
  }

  ...
}


上述 Future是一个抽象类, 但因为有了 factory, 所以也可以直接创建对象.

// 其中 fu 的类型就是  FutureOr<String> ()
var task = ()=> "hello";

// 将 task 丢给 Future, 实际内部返回的是 非公开的 _Future这个实现类
var fu = Future(task);

/**
* 内部会直接以异步的形式 将 task执行, 并将task的返回值传递给 _Future处理
      1. Timer.run          
          相当于异步将 task 添加到 主线程, 这样做会保证 主线程当前的任务不会被阻塞

      2. result._complete(computation());
          其中result就是 _Future, computation就是task
          这里直接 computation() 执行了传递的任务, 并将task的返回值 "hello" 交给 _Future处理
      
      所以 当单纯的调用完 Future(task)后,  Dart内部在取到 task并执行后, 并未直接结束, 而是会做更加深入的操作
      这些操作就是为 Promise的链式调用作准备的
*/


异步操作的返回值

  • 上述简单的Dart使用中, 用户其实关心的是task的返回值"hello". 但因为task的回调是所谓的异步(<font color = green>本质是当前main函数所在的线程执行完所有的任务后再回来取出它来执行</font>), 所以从逻辑上来说, 并不能确定什么时候被回调, 唯一能做的是接着在 task的函数中继续执行相关的业务逻辑
var task = (){
  next_1("hello");
};

var fu = Future(task);


void next_1(value1){
  next_2(value1);
}

void next_2(value2){
  print("over$value");
}

上述的代码没有什么问题, 因为事实上, 只发生了一次异步(task), 但开发中可能遇到这种场景:

用户的3个操作有严格的顺序依赖, 输入输出依赖

  场景:
    合成图层

  有3个图层, layer1, layer2, layer3
  现要求合成3个图层为layer, 并且顺序是 layer1-->layer2-->layer3

  因为合成图层的操作比较耗时, 以当前Dart环境来看, 应该在将每个合成的操作 放在Future中, 类似的代码如下
var layer1 = "layer1";
void main(){
  // 耗时操作, 会阻塞主线程
  String render_layer(String layer, String something) {
    return layer + something;
  }

  Future(() {
    var new_layer1 = render_layer(layer1, "-1");
    Future(() {
      var new_layer2 = render_layer(new_layer1, "-2");

      Future(() {
        var new_layer3 = render_layer(new_layer2, "-3");
        print(new_layer3);
      });
    });
  });
}

这种实现在逻辑上并没有什么问题, 但出现了 <font color = red>嵌套地狱</font>, 具体就不解释了. 所以Dart中提供了改变这种情况的机制:Future的链式调用.这里所说的Future直接理解为JS中的Promise. 先来看一下链式的效果

Future(() => render_layer(layer1, "-1"))
      .then((value) => render_layer(value, "-2"))
      .then((value) => render_layer(value, "-3"))
      .then((value) => print(value));

相比于前面的, 这个可读改更高. 现在来说一下它实现的原理.

  前面说过当调用完 Future(task)后, 在回调task时, 将task的返回值传递给了 _Future, 它后续会做更细致的处理, 这里就不看源码了, 比较简单自己可以看, 直接说步骤:
    1.  将task的返回值 "hello" 传递给 _Future
        result._complete(computation());        result --> _Future,  computation是task
    
    2. _Future内部存储 task的返回值 task_result, 并且通知所有的监听者
        _complete方法中:
          1. _setValue(value as dynamic);                 存储task_result
          2. _propagateToListeners(this, listeners);      通知所有的监听者

    3. 后续 用户通过 Future(task)的返回值 my_future对象调用 then方法时, Dart内部会做相应的处理, 并返回一个新的 _Future, 即调用完then后, 返回
      了一个新的 _Future, 它的细节如下

    3.1 then方法的调用如下
        my_future.then((String value){
            return 100;
        });

    3.2 then方法的声明如下
      Future<R> then<R>(FutureOr<R> f(T value), {Function? onError});
      从声明可以看出:
        它返回一个新的类型 Future<R>
        一个是必传参数  FutureOr<R> (T)
        另一个可选参数
      并且 参数f是方法, 它的返回类型 必须为 then方法返回的Future函数对象的返回值
      如 3.1中的调用then时: 返回的是  Future<int>, 参数是 (String value){ return 100;}
    
    所以 then调用后返回了 Future对象, 则可以继续链式调用




    3.3 then方法的实现
      Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
        Zone currentZone = Zone.current;

        ...

        _Future<R> result = new _Future<R>();
        _addListener(new _FutureListener<T, R>.then(result, f, onError));


        ...
        return result;
      }
    以上是抽取出来的代码, 主要操作是:
      1. 将 新建 _Future<R> 对象, 这个R的类型就是 3.1调用时, 返回的int类型

      2. 调用 _addListener, 该方法内部会 调用参数 f(), 即调用3.1中传递的 (String value){return 100;} 回调方法, 所以用户then传递的回调在这里执行
           其中的value(_FutureListener的relust)在调用时被传递出来, 这个值其实就是 "hello". 与此同时拿到返回值100,存储在了 _Future<R>中, 等待下一次的then调用,再将其传递出来

      3. 将 _Future<R>作为then的返回值给外界

      ps: 第3步后, 用户得到了一个新的 Future<int>, 此时又可以再继续调用 then, 然后内部回调then的参数, 将100作为新的参数传递出来, 再拿到返回值存储在构造新的
        _Future<R>中, 以此循环


Promise出错时回调的原理

  • 上述过程描述了Promise被正确执行的过程, 若中间出错, 在Dart中的then的第2个参数就能用上了
Future<R> then<R>(FutureOr<R> f(T value), {Function? onError});

这里直接说结论: <font color = red>当前then回调抛出的异常, 会在后面出现的enError回调中执行</font>

Future(() {
    return 100;
  }).then((value) {
    print(value);
    throw Exception("hello");
  }, onError: (msg, name) {
    print(msg);
    print("1");
  }).then((value) {   // __code_then_2
    print(value);
  }).then((value) {
    print(value);
  }, onError: (msg, name) {
    print(msg);
    print("3"); //__code_err_3
  });

  // 会在 __code_err_3中被捕获到
  // 若 __code_then_2定义了 onError的回调, 则异常会在这里被捕获到

  // 原因是: 源码中的  _propagateToListeners 方法负责执行当前 then和onError的回调
  // 当执行 then的第1个用户的回调时, 若产生了异常会记录下来, 若后续 继续调用then, 同时传递了onError回调
  // 会根据 记录下来的异常类型, 来匹配 当前遍历到的 onEorror, 匹配到了则直接抛出

  // 结论就是: 当前then第1个 回调中抛出的异常, 不会在第2个参数的 onError中被捕获
  //          会在后续出现的then的 onError作类型匹配, 被条件捕获出来
  // 这个具体自己可以验证


CatchError

  • 除了单独通过then的第2参数用于捕获异常, 也可以通过链式调用一个catchError(Error){}的形式来捕获, 它的原理是一样的, 内部根据_propagateToListeners所记录的异常标识, 来主动调用所提供的回调. 一般将它写在所有then调用的最后
Future(() {
    return 100;
  }).then((value) {
    print(value);
    throw Exception("hello");
  }, onError: (msg, name) {
    print(msg);
    print("1");
  }).then((value) {
    print(value);
  }).then((value) {
    print(value);
  }).catchError((msg, name) {
    print(msg);
    print("4");
  });

await-sync

  • 这个是编译器进一步优化, 允许用户从上往下写代码
void main(List<String> args) {
  var task = (String s, String other) {     // __code_task_future
    return Future(() => s + other);
  };

  () async {      // __code_async

    var r1 = await task("str", "+1");
    var r2 = await task(r1, "+2");
    var r3 = await task(r2, "+3");
    print(r3);
  }();
}

原理是编译器在编译时, 遇到async的函数时, 并不会做异步处理(意味着会阻塞当前线程), 并且会做信号同步等待该函数中所有的await调用, await所调用的函数必须返回Future. 底层的原理本人在c++线程的探究中曾经看过VS2019实现的标准库Future, 在C++的实现Future内部会做锁同步机制, 所以也可以实现从上往下写代码. 这里Dart环境中的原理在底层实现也是一样的


这里解释一下async函数本身是阻塞主线程的

import 'dart:async';
import 'dart:io';

typedef FutureOr<String> Fn();

var task = (String s, String other) {
  return Future(() {
    sleep(Duration(seconds: 2));
    return s + other;
  });
};

void fn() async {
  print("async");
  sleep(Duration(seconds: 5));
  var r1 = await task("str", "+1");
  var r2 = await task(r1, "+2");
  var r3 = await task(r2, "+3");
  print(r3);
}

void test() {
  print("test-start");
  fn();
  print("test-end");
}

void main(List<String> args) {
  test();
  print("over");
}

该案例会在 async函数中阻塞主线程5秒, 但经过 await的task调用后, 会立即返回async函数, 直接先打印test-end和over, 也就是说, 当await的调用的Future中有超时等待, 则Dart会直接让原来的Async函数返回, 最后再按顺序处理所有的Future的返回, 这一点和C++中是不一样的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容