How do async/await/then really work in Dart?

Viewed 926

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.

3 Answers

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...

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.

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.

Related