Flutter —— 异步编程和多线程

Flutter —— 异步编程和多线程,第1张

Flutter —— 异步编程 1. 异步编程1.1 Future1.2 async ,await1.3 Future.then()1.4 Future.catchError1.5 多个Future1.6 Future.wait1.7 microtaskDart的事件循环(event loop) 2. 多线程2.1. isolate2.2 compute 3. 异步和多线程的结合4. Timer5. 多线程和异步使用时机

1. 异步编程 1.1 Future

Future 类,其表示一个 T 类型的异步 *** 作结果。如果异步 *** 作不需要结果,则类型为 Future。也就是说首先Future是个泛型类,可以指定类型。如果没有指定相应类型的话,则Future会在执行动态的推导类型。
在网络请求中经常使用异步编程,那么来探索一下Flutter 中的异步编程。

dart是单线程的,所以底层没有锁之类的东西,但是这不代表着dart不能异步,异步不代表多线程。
下面代码运行后发现做其他事情被堵塞住了,这里async不起作用是因为需要搭配Future使用。

将耗时 *** 作使用Future包装起来,这里可以看到做其他事情就不会被堵塞住了,那么现在即使方法不加async也是异步的,因为Future里面已经是异步的了。或者说async不会异步执行,Future才会异步执行。

如果把 print(‘结束data = $_data’); 放在Future外面那么其就会先于Future里面的代码执行。

1.2 async ,await

那么如何让等待Future里面的执行完在执行后面代码呢?这时候需要用到await。加了await之后,后面的代码就会等待Future里面的执行完之后在执行。这里的await需要搭配async使用,而没有await的情况下,async是没有意义的。await后面的 *** 作也需要是异步的。那么也就是说,Future是用来异步执行的,而async和await搭配是用来让Future里面的某块代码同步执行的。

1.3 Future.then()

Future的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。这里可以使用then来将特定的需要等待的任务放进去,然后不堵塞后面的任务的执行。 如果then没有返回值的话,那么value就是null。

这里看到Future里面return的话那么value就是return的那个值。这个时候,Future里面返回的数据会被Future包装,然后给到了then里面 的value。

1.4 Future.catchError

异步中的错误是用catchError来进行处理的。当异步任务中出现异常之后,
如果调用下列方法就会报错,这是因为任务里面抛出了异常没有处理。

getData() async {
  print('开始data = $_data');
  //耗时 *** 作
  Future future =  Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  });
  future.then((value) {
    print('value = $value');
  });

  print('做一点其他事情');
}

在.then 下面添加代码

  future.catchError((e){print("捕获到了错误:$e");}); 

运行后发现还是报错,但是捕获到了异常。正常来说如果处理了异常,就不应该报错了,那么这里是执行顺序的问题吗?

将catch和then交换位置后运行,发现还是报错

把then注释掉后发现不报错了。

那么这里要怎么处理呢?这里一般用链式调用。

getData() async {
  print('开始data = $_data');
  //耗时 *** 作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  }).then((value) {
    print('value = $value');
  }).catchError((e) {
    print("捕获到了错误:$e");
  });

  print('做一点其他事情');
}

或者在.then中添加onError对错误进行处理

 future.then((value) {
    print('value = $value');
  },onError: (error){print("捕获到了错误:$error");});

onError是在.then这一次对错误进行处理,而catchError则是多次链式调用中的错误处理
如果在.then之前catchError了,那么.then中的value就是catchError中传过来的值了。在catchError之前的.then不会执行。

在CatchError之后添加了whenComplete运行后发现还是报出了异常。

getData() async {
  print('开始data = $_data');
  //耗时 *** 作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  });

      future.catchError((e) {
    print('error = $e');
    return "错误";
  }).then((value) => print('$value'));

  future.whenComplete(() => print("完成了"));

  print('做一点其他事情');
}

这个时候可以在whenComplete之后添加一个catchError,或者使用链式调用。

    future.catchError((e) {
    print('error = $e');
    return "错误";
  }).then((value) => print('$value')).whenComplete(() => print("完成了"));

上面的例子可以看到链式调用能避免大部分的错误,所以在一般都是使用链式调用来进行处理的,并且把catchError放到最后调用,这样出现异常的时候,前面的.then就不会执行了。如果链式太长的话,可以创建方法来让链式更加简洁。

getData() async {
  print('开始data = $_data');
  //耗时 *** 作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  }).then(thenFunc).whenComplete(completeFunc).catchError((e) {
    print('error = $e');
    return "错误";
  });

  print('做一点其他事情');
}

FutureOr thenFunc(String value) {
}

FutureOr completeFunc() {
}

1.5 多个Future

Future是放在队列中的,所以Future的执行是有顺序的。下面的代码结果输出是什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() {
   Future((){
    sleep(Duration(seconds: 2));
    print("C");
  });

  print("B");
}

运行后看到是B->A->C;

那么下面的代码运行后会打印什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() async {
  await Future((){
    sleep(Duration(seconds: 2));
    print("C");
  }).then((value) =>   print("D"));

  print("B");
}

运行后发现是A->C->D->B,这里因为B被C堵塞了,所以A会先执行,然后C执行完之后执行D,最后执行B。

那么下面代码的输出结果是什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() async {
   Future((){
    return "任务1";
  }).then((value) =>   print("$value结束"));

   Future((){
     return "任务2";
   }).then((value) =>   print("$value结束"));

   Future((){
     return "任务3";
   }).then((value) =>   print("$value结束"));

   Future((){
     return "任务4";
   }).then((value) =>   print("$value结束"));

  print("任务添加完毕");
}

运行后发现是任务添加完毕->A->任务1结束->任务2结束->任务3结束->任务4结束。

那么这里任务一定是按顺序执行的吗?在任务二添加sleep后重新执行,发现任务顺序还是一样的。

这说明这里会按异步任务的添加顺序执行的。

1.6 Future.wait

当需要等待多个Future完成,并收集它们的结果,可以使用Future.wait。这个时候.then就会等wait里面所有任务完成后在执行,然后返回的值可以用数组来取。wait里面的任务同时处理,但是是按添加顺序执行的,而如果是链式执行的话,则是一个执行完在执行下一个。

 Future.wait([
  Future((){
    return "任务1";
  }),
  Future((){
  return "任务2";
  }),
  Future((){
  return "任务3";
  }),
  Future((){
  return "任务4";
  }),
  ]).then((value) => print(value[0] + value[1] + value[2]   ));
1.7 microtask Dart的事件循环(event loop)

在Dart中,实际上有两种队列:

事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。

因为microtask queue 的优先级高于 event queue ,所以如果 microtask queue有太多的微任务, 那么就可能会霸占住当前的event loop。从而对event queue中的触摸、绘制等外部事件造成阻塞卡顿。

在每一次事件循环中,Dart总是先去第一个microtask queue中查询是否有可执行的任务,如果没有,才会处理后续的event queue的流程。

异步任务我们用的最多的还是优先级更低的 event queue。Dart为 event queue 的任务建立提供了一层封装,就是我们在Dart中经常用到的Future。

正常情况下,一个 Future 异步任务的执行是相对简单的:

声明一个 Future 时,Dart 会将异步任务的函数执行体放入event queue,然后立即返回,后续的代码继续同步执行。
当同步执行的代码执行完毕后,event queue会按照加入event queue的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的 *** 作。
上面说过了microtask队列的优先级比较高,那么使用microtask就可以让任务优先执行。
下面代码的先执行代码1,代码2,A以及B。

void testFuture3() {
  print('外部代码1');
  Future(()=>print('A')).then((value) => print('A结束'));
  Future(()=>print('B')).then((value) => print('B结束'));
  print('外部代码2');
}

添加了微代码后,那么微任务就会在异步任务之前执行。

void testFuture3() {
  print('外部代码1');

  Future(()=>print('A')).then((value) => print('A结束'));
  Future(()=>print('B')).then((value) => print('B结束'));
  scheduleMicrotask(() {
    print("微任务A");
  });
  print('外部代码2');
}

下面的任务执行后打印情况是什么样的呢?这里5一定是先执行的,然后执行微任务3,然后异步任务按照添加的顺序执行,那么就会先执行future1打印1和4,最后执行future2 打印2,所以打印 5 —— 3 —— 1 —— 4 —— 2;

void testFuture4() {
 Future future1 =  Future((){print('1');});
 Future future2 =  Future((){print('2');});
 scheduleMicrotask(() {
   print("微任务3");
 });
 future1.then((value) => print('4'));
 print('5');
}

运行后验证果真是的。

那么这里的打印顺序是什么呢?这里future3最先被添加到队列,所以依然会比 1, 2 优先执行,所以会先打印6,所以打印 5 —— 3 —— 6 —— 1 —— 4 —— 2;

打印结果:

那么如果是这样的话,打印结果会是什么呢?按照图片里的,执行future3的时候会把自身任务执行完再重新开始循环,那么也就是说,这里5,3之后会先打印6,8 ,然后再打印7,然后再打印142。

void testFuture4() {
  Future future3 = Future(() => null);
  future3.then((value) {
    print('6');
    scheduleMicrotask(() {
      print("7");
    });
  }).then((value) =>  print("8"));

  Future future1 = Future(() {
    print('1');
  });

  future1.then((value) => print('4'));
  Future future2 = Future(() {
    print('2');
  });
  scheduleMicrotask(() {
    print("3");
  });

  print('5');
}

打印结果:

那么把.then拆出来的话是什么结果呢?其实这里相当于把.then里面的任务放到微任务里面去了,所以8依然会优先执行。这也是为什么 future1里面的then的任务会比 future2先执行。

  Future future4 =  future3.then((value) {
    print('6');
    scheduleMicrotask(() {
      print("7");
    });
  });
  
  future4.then((value) => print("8"));

打印结果:

2. 多线程 2.1. isolate

Flutter默认是单线程任务处理的,如果不开启新的线程,任务默认在主线程中处理。和iOS应用很像,在Dart的线程中也存在事件循环和消息队列的概念,但在Dart中线程叫做isolate。应用程序启动后,开始执行main函数并运行main。isolate。

下面的打印结果一定是123,因为这里Future会等待主线程睡眠结束。

void IsolateDemo() {
  print('1');
  Future(func);
  sleep(Duration(seconds: 2));
  print('2');
}

FutureOr func() {
  print('3');
}

那么如果将func放在Isolate里面执行,那么在打印2之前func就会先打印3。

void IsolateDemo() {
  print('1');

  Isolate.spawn(func,10);
  sleep(Duration(seconds: 1));
  print('2');
}


void func(int message) {
  print('3');
}


多添加几次任务来执行。

void IsolateDemo() {
  print('外部代码1');

  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  Isolate.spawn(func,10);
  Isolate.spawn(func2,10);
  sleep(Duration(seconds: 1));
  print('外部代码2');
}

运行后发现这里任务一不一定比任务2先执行,而且外部代码也会在任务一任务二之前执行,证明了这里Isolate确实是子线程里面执行任务的。

Dart中的isolate更像是一个进程,因为isolate有独立的内存空间,意味着每个isolate中的数据是独立的,所以没有资源抢夺的问题,也就不需要锁。所以,我们访问数据不能直接访问。isolate相当于轻量级的进程,他不会独立开辟堆和栈,而是给了一个局部的内存空间,所有的变量,内存对象都在这个空间里面,和原来的程序传递数据的时候就需要用到进程间的通讯。
声明一个属性没然后在func里面赋值,在IsolateDemo里面睡眠之后打印a的值看一下是否改变。

int a = 10;
void IsolateDemo() {
  print('外部代码1');

  Isolate.spawn(func,1000);

  sleep(Duration(seconds: 1));
  print('外部代码来了a=$a');
  print('外部代码2');
}

void func(int message) {
  a = message;
  print('第一个来了a=$a');
}

运行后发现是没有改变的

那么如果非要改变a的值的话,那么就需要用到port,将port的sendPort作为参数传给func,然后添加listen监听数据变化来接受func里面传过来的值。

void IsolateDemo() {
  print('外部代码1');
  //  创建一个port
  ReceivePort port = ReceivePort();
// 创建一个Isolate
  Isolate.spawn(func,port.sendPort);
  //监听数据变化
  port.listen((message) {
    a = message;
    print('接受到了a=$a');
  });

  sleep(Duration(seconds: 1));
  print('外部代码来了a=$a');
  print('外部代码2');
}


void func(SendPort send) {
  send.send(100);
  print('第一个来了a=$a');
}

运行后发现a的值改变了。

既然我们Isolate开辟了空间,那么我们就要手动去销毁Isolate。 这里创建临时变量iso,然后在port.listen里面关闭端口以及杀掉iso。这里的await不会堵塞后面代码的执行,因为这里是其他线程里面的。

void IsolateDemo() async {
  print('外部代码1');
  //  创建一个port
  ReceivePort port = ReceivePort();
  // 创建一个Isolate
 Isolate iso =  await Isolate.spawn(func,port.sendPort);

  //监听数据变化
  port.listen((message) {
    a = message;
    print('接受到了a=$a');
    port.close();
    iso.kill();
  });
  print('外部代码来了a=$a');
  print('外部代码2');
}

2.2 compute

关于Iso还有一个封装叫compute,这里使用compute之后发现这里并不会堵塞住compute的执行。

void computeTest() {
  print('外部代码1');
  compute(func1,10);
  sleep(Duration(seconds: 2));
  print('外部代码2');
}
FutureOr func1(message) {
  print('compute');
}

运行结果:

compute和iso不一样的是,如果加了await,那么后面的代码就需要等待。并且compute可以返回数据来修改数据的值,但是如果在func1修改a的值的话同样是没有效果的。

void computeTest() async{
  print('外部代码1');
  a = await compute(func1,10);
  print('外部代码2 a = $a');
}
int func1(message) {
  sleep(Duration(seconds: 2));

  print('compute');
  return 1000;
}

3. 异步和多线程的结合

下面的打印是同步还是异步呢?

void isoLoadDemo() {
  Future( () => compute(testfunc,123)).then((value) => {print('1结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('2结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('3结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('4结束')});
  Future( () => compute(testfunc,123)).then((value) => {print('5结束')});

}

运行后看到这里是异步的。

那么如果是这样的呢?

void isoLoadDemo() {
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('1结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('2结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('3结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('4结束')});
  Future(() {
    compute(testfunc, 123);
  }).then((value) => {print('5结束')});
}

运行后发现这里是同步的了。

这是为什么呢?因为箭头函数其实是return compute(testfunc, 123) 的,如果在下面的代码添加return之后,那么就是异步的了。如果返回的是子线程的Future,那么.then处理的是子线程的Future的结果,否则就是当前Future的结果。
那么 compute(testfunc, 123) 的执行是有序的还是无序的呢?在Future里面添加打印

void isoLoadDemo() {
  Future(() {
    print('1开始');
    return compute(testfunc, 123);
  }).then((value) => {print('1结束')});
  Future(() {
    print('2开始');
    return compute(testfunc, 123);
  }).then((value) => {print('2结束')});
  Future(() {
    print('3开始');
    return  compute(testfunc, 123);
  }).then((value) => {print('3结束')});
  Future(() {
    print('4开始');
    return compute(testfunc, 123);
  }).then((value) => {print('4结束')});
  Future(() {
    print('5开始');
    return compute(testfunc, 123);
  }).then((value) => {print('5结束')});
}

运行后发现开始是按顺序打印的,那么就说明compute(testfunc, 123)是按顺序执行的,但是返回的时机是无序的。

那么同理可知,如果去掉return, 那么就会按顺序打印1开始,1结束 … ,运行后证明是正确的。

其实.then任务和Future可以当作是一个整体。下面代码中,按道理应该是Future里面的微任务1先被添加到队列中,然而实际上却是微任务先执行,所以说这里可以把.then和Future当作是一个整体。

 Future x = Future((){
    print('异步任务1');
    scheduleMicrotask(() {
      print('微任务1');
    });
  });

  x.then((value) {
    print('微任务2');
  });


这里whenComplete也是一样的,并且如果whenComplete在then前面,那么就先于.then执行,否则就后于.then执行。

4. Timer

Timer会默认开启一个异步任务。

void main() {

  Timer.run(() {
    print('异步任务');
  });

  print('来了');
  }

运行后

回到之前的聊天界面,试一下timer是否会卡住ui。
在initState里面添加一个timer

  int _count = 0;
    Timer.periodic(Duration(seconds: 1), (timer) {
      _count ++;
      print('$_count');
      if (_count == 99) {
        timer.cancel();
      }
    });

然后在timer启动后拖动ListView,发现timer是没有像ios里面一样是暂停的。但是这里有一个小问题,之前聊天界面是保存状态的,如果没有保存状态的话,那么重新进入聊天页面,就会重新走一个init,重新创建一个timer,而前面的timer没有被取消,那么就会有多个tiemr存在的情况。

这个时候就需要用到dispose,当State 被永久的从视图树中移除时,Flutter 会调用dispose 函数。
添加一个变量timer,然后给timer赋值。

  late Timer _timer;
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      _count ++;
      print('$_count');
      if (_count == 99) {
        timer.cancel();
      }
    });

在dispose中判断如果_timer有值且正在运行,那么就将其取消掉。

 @override
  void dispose() {
    // TODO: implement dispose

    if (_timer != null && _timer.isActive) {
      _timer.cancel()
    }

    super.dispose();
  }
5. 多线程和异步使用时机

什么时候该使用多线程,什么时候该使用异步呢?
在聊天界面的AppBar的actions添加一个GestureDetector,里面是一个添加按钮,然后在onTap里面添加一个异步的耗时 *** 作。

 GestureDetector(
            child: Container(
              child: Icon(Icons.add),
            ),
            onTap: () {
              Future(() {
                print('开始');
                for (int i = 0; i < 1000000000; i++) {
                }
               print("结束了");
              });
            },
          ),

点击后发现主线程完全被卡死了,页面滑动不了,并且timer也不动了。这个时候就需要将耗时 *** 作放在子线程执行,这样主线程就不会被卡住了。

onTap: () {
              Future(() {
               return compute(func,123);
              });
            },
 FutureOr func(int message) {
    print('开始');
    for (int i = 0; i < 1000000000; i++) {
    }
    print("结束了");
  }

总结:

Dart中的异步 Future对象来完成异步 *** 作。 通过工厂构造方法创建Future对象。参数为Dart的函数 函数的执行代码将被放入事件队列异步执行。 async 和 await 。如果Future内部代码希望同步执行,则使用await修饰。被async修饰的函数为异步执行。Future结果处理 Future.then 用来注册一个Future完成时要调用的回调Future.catchError注册一个回调,来捕捉Future的error Future.catchError回调只处理原始Future抛出的错误,不能处理回调函数抛出的错误onError只能处理当前Future的错误 Future.whenComplete 在Future完成之后总是会调用,不管是错误导致的完成还是正常执行完毕。 Dart的事件循环 在Dart中,实际上有两种队列: 事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。 Dart中的多线程 Dart是单线程语言,但并不代表它不能并行执行代码。因为它拥有Isolate。Isolate Isolate可以看成是一个小的进程。 它拥有独立的内存空间,不同Isolate之间通过消息进行通信它拥有自己的事件循环及队列(MicroTask 和 Event) Isolate使用 1、创建子线程任务:Isolate.spawn(arg1,arg2); arg1: 需要在子线程执行的函数arg2:传递给函数的参数这样就在另一个线程中执行arg1的代码了。 2、端口通讯 ReceivePort port = ReceivePort()//构造一个端口。port.listen(arg)//注册一个回调监听 arg为回调函数。参数为消息内容 在子线程中.通过port.send() 发送消息 3、关闭端口,销毁Isolate 注意端口使用完毕需要调用port.close()函数关闭Isolate使用完毕,需要调用Isolate.kill()函数销毁 compute 由于dart中的Isolate比较复杂,数据传输比较麻烦,因此flutter在foundation库中封装了一个轻量级compute *** 作使用:compute(func,count) func:子线程函数!func如果有返回值会直接被compute函数返回出去!count: 给函数的参数 异步多线程结合 Dart中的异步是可以和多线程结合使用的。如果Future中返回子线程的返回值。那么Future的处理是异步的如果Future中没有返回子线程的返回值。那么Future的处理是同步的Future的结果处理会在Future执行完毕立即执行。可以看做是一个任务。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/996917.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存