4

This is maybe a recurring question but I found conflicting answers and I'm now confused as to which of them is the correct one. I thought I understood the concept then I started reading all of those answers and got totally confused so I'm looking for a definite and simple answer to my question that I could easily comprehend.

As per this answer and this article, await is supposed to interrupt code execution and actually wait for the future to complete and then continue executing the rest of the code sequentially. It also suggests that this might block the main thread, which is only logical in that case.

On the other hand, this, this and this video from the flutter team suggest that await is not going to block the rest of code execution and that it's just syntactical sugar to register callbacks to be executed when the future finishes, which is the same thing that then does.

Now, I tried to write a small program to understand which of them is correct and it seems that the first approach is the way to go:

import 'dart:async';

// prints: 
// 1000+
// 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1)).then((_){print(watch.elapsedMilliseconds);});
  
  print(watch.elapsedMilliseconds); 
  
}

In opposition to:

import 'dart:async';

// prints:
// 0
// 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  Future.delayed(Duration(seconds:1)).then((_){print(watch.elapsedMilliseconds);});
  
  print(watch.elapsedMilliseconds);
  
}

So I just want to know why the flutter team and some people are suggesting that await does not block the code execution and how this concept really works.

moazelshebly
  • 145
  • 9
  • I think you are confusing threading with the event loop. As you know, there's only on thread but that thread can seem to execute lots of things at once because of the event loop. Any of the things it is executing can pause and (a)wait for things because they relinquish the thread. This allows other things to interleave as their events (timers, network, i/o, etc) complete. For more on the event loop see: https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a – Richard Heap Dec 16 '21 at 17:46
  • The only way you can block the thread is by doing something compute bound (calculate pi to 10000 places, calculate a digital signature, decode a large image, etc). This is why Dart has isolates. This is the way that you can create a second thread that can consume all of a core doing the compute intensive thing, while leaving your original main isolate able to react to events. – Richard Heap Dec 16 '21 at 17:48

3 Answers3

12

I think there is a bit of misunderstanding about blocking. When you look at your first example - await will block only the rest of your code in your function from executing. The rest of your app will still work just fine.

You need to understand one thing: async/await syntax is just a syntatic sugar for .then(callback) syntax. They both achieve the same thing, only async/await is a lot easier to read, debug and understand. As you can see - in both of your examples you are getting the same result. The question for you is: which syntax do you prefer?

To clarify - let's assume that you want to introduce several wait events of 1 second, and write your a message after each one of these.

Your first example will look like this:

import 'dart:async';

// prints 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1));
  print(watch.elapsedMilliseconds); 

  await Future.delayed(Duration(seconds:1));
  print(watch.elapsedMilliseconds); 

  await Future.delayed(Duration(seconds:1));
  print(watch.elapsedMilliseconds); 

  
}

Note how easy is to read the code and understand.

Now, to the second example changed to achieve the same thing:

import 'dart:async';

void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  Future.delayed(Duration(seconds:1)).then((_){
    print(watch.elapsedMilliseconds);
    Future.delayed(Duration(seconds:1)).then((_){
        print(watch.elapsedMilliseconds);
        Future.delayed(Duration(seconds:1)).then((_){
             print(watch.elapsedMilliseconds);
        });
    });
  });
}

They will both achieve the same - but the second example makes your eyes hurt.

One more interesting scenario for you to consider is - what if you want several things happening at the same time? And this is not unusual - if you needed to fetch 3 images from 3 different servers, you would not fetch them sequentially. You would want to fire all 3 requests at the same time, and wait for all of them to finish.

Using the async/await this is very easy:

import 'dart:async';

// prints 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  var f1 = Future.delayed(Duration(seconds:1));
  var f2 = Future.delayed(Duration(seconds:2));
  var f3 = Future.delayed(Duration(seconds:3));

  await Future.wait([f1, f2, f3]);

  print(watch.elapsedMilliseconds); 

  
}

Note that since we don't put await in front of each Future.delayed - which means we will start the delayed future, but we will not wait for it's completion.

You will see that the whole function takes only 3 seconds to complete; since all 3 timers are running at the same time. Future.wait will wait for a list of futures to complete.

Now - it is pretty clear that you don't really need .then() syntax in most of the cases, but I think it will still be applicable in more complex scenarios.

For example: you need to fetch 3 images from 3 servers. Each of those servers has a backup server; if the first server returns null as a result - you need to fetch the resource from the backup server. Additionaly: if Backup server 1 or Backup server 2 returned null, you need to call server 4 to get a single image.

You could even plot a small graph describing this. Now this is where .then() syntax comes in handy - and we will still combine it with async/await. I think once you fully understand this example - you pretty much understand async/await and .then(). Let's go:

import 'dart:async';
import 'dart:math';

Future<int?> getImage(String server) async {
  var rng = Random();
  
  print("Downloading from $server");
  
  // we'll add random delay to simulate network
  await Future.delayed(Duration(seconds: rng.nextInt(5)));
  
  print("$server is done");
  
  // high chance of returning null
  if (rng.nextInt(10)<7) return null;
  return 1;
}

// prints 1000+
void main() async {
  
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  // get the image from server 1
  var f1 = getImage("Server 1").then((data) async { 
     return data ?? await getImage("Server 1 backup");
  });
  
  var f2 = getImage("Server 2").then((data) async { 
     return data ?? await getImage("Server 2 backup");
  });

  var f4=Future.wait([f1, f2]).then((data) async {
    if (data[0]==null || data[1]==null) {
       return [await getImage("Server 4")];
    } else {
       return data;
    }
  });
  
  var f3 = getImage("Server 3").then((data) async { 
     return data ?? await getImage("Server 3 backup");
  });

  await Future.wait([f3, f4]);

  print("elapsed ${watch.elapsedMilliseconds} ms"); 
  
}

One new thing here is: .then() will return a future object - which you can still wait with await keyword. Told you it was the same thing....

Without .then() syntax, you would need to create one more async function to handle this, making your code a bit mode complex and more difficult to read. With .then() syntax the code is just a bit more managable. See, again - .then() and async/await are practically the same thing...

Standard async/await helps when things are linear (like in multiple Future.delayed exapmle I showed). But when you get to a complex scenario that can be described via Graph with multiple branches running in parallel - .then() will come in handy.

Edit - Dart being Single Thread

And on Dart being single threaded, think about it this way: your code runs inside Dart engine (or Dart VM), and this code really is single threaded. But any call to the outside world will be run in parallel (calling a remote server, or even calling a local hard-drive, calling other process on the same host like OS - and yes, even calling the Timers like in my example).

Like in my example above: I called 3 remote servers to fetch something, and I chained 3 different callbacks, 1 for each call. And the 'outside world things' - calling the servers - is really happening in parallel. Single threading of Dart simply guarantees that only one line of my code will be executed at any given point of time.

If you came from Java background, you would know how difficult was in Java to synchronize multiple threads: and this is where the code would often break. In Dart, you don't need to worry about this. The real performance optimization is the fact that anything happens outside of Dart VM is really running in parallel - and Dart takes care of it for you.

Now how does this work: event loop. That's a little dart engine that keeps track of all your remote server calls, and when ready calls back your - well, callback procedure. Event loop is the one that takes care that your code processes one request at the time...

Andrija
  • 1,534
  • 3
  • 10
  • Thank you for your very thorough answer. However, I don't agree with you that both my examples produce the same result. I edited the code in the first example and chained a .then() to the future which makes the program print 1000+ two times instead of 0 and 1000+. Your last example is nice, though. I can pretty much understand that we can use .then() for fallback scenarios or to maybe use the result of the first future to do something else with it in the then clause. My only problem is: how does continue executing outside my function -as you mentioned- if dart is single-threaded? – moazelshebly Dec 16 '21 at 15:16
  • You are right - the exact output will not be the same in both of your cases. The question is: what did you want to logically achieve? If your goal was for last print statement to print the total duration of your program - then the first approach is the correct one. Or to understand it in a different way: if there is a dependency so the 2nd print can run only after the first one was completed - then the 1st approach is correct. If there is no dependency - then logically both are fine; but in the 2nd approach you executed 2nd print command much earlier - optimizing the whole process. – Andrija Dec 18 '21 at 10:53
  • I forgot to reply to your comment as I was busy writing my answer. Your explanation and answer helped me get to my current understanding of how this all works. It is indeed all based on what you want to achieve in the end and what your goal is. You have to really understand how you want the code to flow inside your program and get a good grasp of how the event loop works. Only then you'll be able to achieve what you're looking for. Thank you for the great answer! – moazelshebly Dec 20 '21 at 11:38
  • Thank you very much for these really nice explanations - as well as the self-answer by @moazelshebly . I like specially the part when you, @andrija, mention that `then` can be used when there is a dependency relationship. However, that doesn't seem to be applicable when the function to wait for returns `Future` since `then` does not accept such as function as input. So, to cover that specific use case, it would seem that `whenComplete` needs to be used, rather than `then`. Would this be correct? – dalonsoa Jun 20 '23 at 09:52
  • No need for that; 'then' can handle Future just fine. – Andrija Jun 20 '23 at 10:33
1

The answer from Andrija is technically correct. However, I still had to think about it a lot until I was able to understand how it really works and this is why I will try to simplify things for anyone who might have the same question.

Suppose you have a dart program; obviously with a main(). Our program calls two functions; foo() and bar().

The function foo() does some asynchronous work, e.g. a network call:

Future<void> foo() async{
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1));
  
  print(watch.elapsedMilliseconds);
}

And the function bar() is a normal function that executes some synchronous code:

void bar() {
  print("Some synchronous code");
}

Now, suppose your main() looks like this:

void main() {
  foo();
  bar();
}

The main program starts and foo() is called -without an await in main()-, we hit the await in foo() and the program goes: "Oh! I'm not supposed to delay the rest of the execution in main(). I gotta register a callback to be executed when the async work is done and go continue the execution of main()". foo() is popped off of the call stack and bar() is then called and prints "Some synchronous work" and is also popped off of the call stack. In the meantime, the async work in foo() finishes and signals completion. This gets picked up by the event loop which goes back to executing the rest of the code in foo() (or the code in the callback if we use .then(); of course if the main thread is not busy.

And this is in simple words what happens. As Andrija suggested, the await blocks the rest of code execution in the same function; the rest of your program will run just fine. If we'd used an await in main() to a-wait for foo(), the execution in main() would've also been blocked until the async work in foo() is done which is not what I had initially thought.

My thinking was that the code in main() will also be delayed based on the await in foo(), which is not the case as we've seen.

moazelshebly
  • 145
  • 9
  • Spot on! Great explanation! I like you saying "I still had to think about it a lot" - you can read as many tutorials or stackoverflow answers as you want - but until you really "think about it a lot" - you will not get it. One more advice - I realized that I would write a buggy code initially-and I never knew if my logic was wrong, or the way I handled async/await. I realized - If I put await on every single async call, it allowed me to focus on my logic. After it worked, I would start optimizing the performance by removing await, and figuring out how to utilize event loop. – Andrija Dec 20 '21 at 05:31
  • Great tip! I will try to apply it next time I write async code. Thank you :) – moazelshebly Dec 20 '21 at 11:39
0

Actually your both functions are having same Result, let me explain more clearly..

When calling async functions they simply don't block our other part of the application to render. Whatever action we are performing inside will be delayed but the rest part will work as it is.

Now let's come to your examples

import 'dart:async';

// prints 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  await Future.delayed(Duration(seconds:1));
  
  print(watch.elapsedMilliseconds); 
  
}

In the above example you are just passing the delay duration and not the callback. So it's treating rest of the part as it's callback which will be called once the duration is completed. Now what are you doing that you are telling your function to wait for a Duration you have provided the execute the further code.

So the result is 1000+

In the below example

import 'dart:async';

// prints:
// 0
// 1000+
void main() async {
  Stopwatch watch = Stopwatch();
  
  watch.start();
  
  Future.delayed(Duration(seconds:1)).then((_){print(watch.elapsedMilliseconds);}); // prints 1000+
  
  print(watch.elapsedMilliseconds); // prints 0
  
}

you are properly assigning a callback to Future. So now Future will only hold it's callback the let the rest of the part completed.

This is the reason that it's printing 0 first then after delay of a second it prints 1000+.

And Future and Future delay has different work flow this could be now right way to use await or not.

Diwyansh
  • 2,961
  • 1
  • 7
  • 11
  • Still, if you assign a callback in the first example to print `elapsedMilliseconds`, it's going to print: 1000+ and then another 1000+. This suggests that if you don't use `await` you're not actually blocking code execution. On the other hand if you use `await, it will first print 0 and then 1000+, which is proof that code execution is interrupted. – moazelshebly Dec 16 '21 at 13:04
  • You should go through the link to get proper details and use cases of Future along with the example it will also let you know how to properly use the Future also consider that are you using the Future in correct way. Please have a look https://dart.dev/codelabs/async-await – Diwyansh Dec 16 '21 at 13:26