Home > OS >  What exactly await does internally in Dart?
What exactly await does internally in Dart?

Time:01-20

I'm working on a flutter application which requires using local storage. As the document directory path in Android will be given in Future<Directory>, I have to check whether the future finishes or not, each time I want to use the path.

The code may be similar to below

class DataStructure {
  late Future<Directory> _dir = getApplicationDocumentsDirectory();

  Future<void> read(String fileName) async {
    Directory dir = await _dir;

    // Do something using dir
  }
}

This might be stupid, but I write C code in most of the time, so I want to reduce number of times pushing a function to the event queue for a better performance (I guess the compiler cuts async functions with await into "functions" to be pushed to the event queue). So I wrote experiment code to check whether await on a finished future will cut the control flow or not. (I mean, in the newest version of Dart, async functions will execute until the first 'await' keyword arises. I'm wondering if this is still the case if the future has already been finished.)

Future<void> voidFuture(String name) async {
  print('$name: voidFuture');
}

Future<void> printSingle(String name, int index) async {
  print('$name: -- index = $index');
}

// This emit function will print one line each time
// it gets chance to be executed.

// Because the 'await' keyword will cut the control flow,
// it can only print once each time it's executed,
// and must wait for the next chance to print again.

// This feature makes it appropriate for testing
// when and where the control flow is cut,
// as each cut will lead to one line of output.
Future<void> emit(String name, int count) async {
  for (int index = 0; index != count;   index) {
    await printSingle(name, index);
  }
}

Future<void> task_0() async {
  const String name = 'task_0';
  Future<void> emitFinish = emit(name, 3);
  await voidFuture(name);
  print('$name: output after await');
  await emitFinish;
}

Running task_0 in my environment (Dart SDK version: 2.18.5 (stable) on "windows_x64") gives output as below:

task_0: -- index = 0
task_0: voidFuture
task_0: -- index = 1
task_0: output after await
task_0: -- index = 2

Which is the same as what I expected. And the weird thing comes when I change the emit() function:

Future<void> emit(String name, int count) async {
  for (int index = 0; index != count;   index) {
    // Before:
    // await printSingle(name, index);
    // After:
    await Future(() {
      print('$name: -- index = $index');
    });
  }
}

Then the output becomes

task_0: voidFuture
task_0: output after await
task_0: -- index = 0
task_0: -- index = 1
task_0: -- index = 2

and it makes no sense for me that the third line, -- index = 0 comes after output after await. It seems that a function is more privileged than a future from constructor?

And my main question is "Will 'await' waits for a finished future?", so I wrote code below:

Future<String> stringFuture() async {
  return '.';
}

Future<void> task_3() async {
  const String name = 'task_3';
  Future<void> emitFinish = emit(name, 4);
  Future<String> futureString = stringFuture();
  print('$name: before await of futureString');
  await futureString;
  print('$name: 1st await of futureString over');
  await 1;
  print('$name: 1st await of constant over');
  await 2;
  print('$name: 2nd await of constant over');
  await emitFinish;
}

With the first version of emit(), the output is

task_3: -- index = 0
task_3: before await of futureString
task_3: -- index = 1
task_3: 1st await of futureString over
task_3: -- index = 2
task_3: 1st await of constant over
task_3: -- index = 3
task_3: 2nd await of constant over

Which means even await for a constant integral will push the lines after it to the event queue.

(Of course, with the second version of emit() all its output comes after the last print() in task_3() , and I don't know why)

I know there are manys work-arounds, one of them will be using a T? value to be assigned after the first time the Future<T> finishes, and check whether value == null each time using it.

But the questions I want to ask are:

  • What does the keyword await do internally? Please come with details that are enough to explain phenomena above.

  • Is there any way of overriding the default await behavior? E.g., by overriding a method?

  • What's the preferred way of using a Future value for many times?

  • (Unrelated to above) How to stop at the welcome page in Flutter to wait for these async functions, e.g.,

    getApplicationDocumentsDirectory()
    

    to finish before building all the widgets?

Most results I got from Google were introduction on async and await keywords for beginners, and I couldn't find much material explaining the behavior of await in Dart API Reference Documentation.

Thank you for saving a heart broken by await >_<

CodePudding user response:

await is syntactic sugar for registering callbacks through the Future API. For example:

Future<int> foo() async {
  var x = await someIntFuture();
  return otherStuff(x);
}

is basically transformed into:

Future<int> foo() {
  return someIntFuture.then((x) {
    return otherStuff(x);
  });
}

await registers a Future.then callback and returns that new Future. That it returns a Future means that await always yields (even in cases such as await null). This is also why when you invoke an async function, its body is executed synchronously until it reaches its first await.

it makes no sense for me that the third line, -- index = 0 comes after output after await. It seems that a function is more privileged than a future from constructor?

From the documentation for the Future constructor:

Creates a future containing the result of calling computation asynchronously with Timer.run.

The callback you supply to the Future constructor is invoked asynchronously; it is scheduled. This is different from calling an async function, which as stated before executes synchronously as much as possible first.

And my main question is "Will 'await' waits for a finished future?"

It doesn't matter if the Future is already completed or not. await always yields.

Is there any way of overriding the default await behavior? E.g., by overriding a method?

As mentioned, await is syntactic sugar. What you could do is to create a class that implements the Future API and handles .then differently (which is what Flutter's SynchronousFuture class does), but I wouldn't recommend it (for the same reasons why the SynchronousFuture documentation discourages its use).

What's the preferred way of using a Future value for many times?

Depends on the situation. In general, try to await the Future once and store the result somewhere (such as in a local variable). Otherwise I'd just await the Future multiple times and not worry about it until there's evidence that it's performance-critical.

(Unrelated to above) How to stop at the welcome page in Flutter to wait for these async functions

Depends. For some things, you can simply make your main function async and await whatever asynchronous initialization you want to do before calling runApp. In other cases (particularly for long-running ones), you should use a FutureBuilder.

(Also, in the future, separate questions should be asked separately.)

  • Related