-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Future.value: consider completing the future immediately, without scheduling an extra microtask #49381
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
There should be no visible difference between a If anything, the listener is notified sooner if it happens to be added between the scheduling of the microtask in It is a strong promise that a future will not call the callback during the The What is possible is optimizing In short, those microtasks are often not unnecessary, and very often something people depend on. Even the tiniest change is a heck to clean up. |
It's not obvious why Writing the code
and assuming that
Maybe we should think about a completed Future as merely a box over a value and make sure all operations with a completed Future are always synchronous. |
The reason If the callback can happen during the The same for streams. The stream API allows you to do The
That's a very different thing from a Dart future. A Dart future deliberately tries to hide whether it's already completed, because experience tells us that if we don't, people will start writing two code paths, one for the synchronous case and one for the asynchronous case, whether they actually need the speed or not (because who doesn't want speed?) I'm completely fine with optimizing |
Okay, let's leave @lrhn Can we make |
I agree with @lrhn that the default Future shouldn't act like SynchronousFuture. The warning at the end of the API docs quoted above is not just a boilerplate disclaimer. It's REALLY CONFUSING to debug issues involving SynchronousFuture and we only use it in very specific circumstances where the performance benefits vastly outweigh the cost. (Actually it's not even the performance benefits that we want, it's specifically the synchronicity. The whole point is to be able to get data out of a potentially asynchronous operation like an image load without interrupting the synchronous build process, when the data has already been obtained and cached.) |
We can definitely make It won't necessarily make anything faster.
This is an optimization tradeoff. Which behavior is most common? It probably is "one immediate listener" or "no listener", which means that making the change is going to be a tiny delay for the one listener (but also quite likely no delay at all, since it's entirely possible that no other microtasks would be scheduled between creating the future and calling So, yes, we could change the behavior. (Which is why I already did that in the refactoring.) About making |
Regarding Having an extra microtask between the Let's checkout this example: class AppState {
final int a;
final int b;
AppState({
required this.a,
required this.b,
});
AppState copyWith({
int? a,
int? b,
}) {
return AppState(
a: a ?? this.a,
b: b ?? this.b,
);
}
}
void main() {
test('Should not swallow an action', () async {
AppState state = AppState(a: 0, b: 0);
Future<AppState> completedFuture() async {
// await Future.microtask(() {}); <-- UNCOMMENT THIS TO PASS THE TEST
return state.copyWith(a: state.a + 1);
}
Future<AppState> regularFuture() async {
await Future.microtask(() {});
return state.copyWith(b: state.b + 1);
}
scheduleMicrotask(() async {
// simulate race condition
// schedule later so that reducer1 and reducer2
// are returning in the same micro task.
state = await completedFuture();
});
state = await regularFuture();
await Future.delayed(Duration.zero);
// This fails because there is a microtask between the return statement
// of reducer1 and the assignment to the state.
expect((state.a, state.b), (1, 1));
});
} Is there any way to get this behaviour of getting the result immediately? I needed to add a warning in a library for developers since this issue is unsolvable right now: |
@Tienisto If you avoid writing code with specific timing dependencies, then you won't have any of these issues. Problem solved 😉 That said, this is not really about (And what this issue is about is whether |
Thanks for the quick response! In my example, I only counted micro tasks to force a race condition. As you have noticed, it is about completing immediately or completing in the next micro task.
The second case is most common (that's why you use async functions). There is a similar issue in async_redux documented in this comment: Well, in |
Likely the same underlying issue, of code depending on a particular scheduling of microtasks. There is no way to get a future callback immediately when a computation completes, not unless that computation uses a That's what an But, and I can't say that strongly enough, there is no promise that the future will be completed synchronously when an Asynchronous code does not make any guarantees other than that something will eventually happen, and some ordering promises. There is absolutely nowhere in the specification where anything is specified to happen in a specific microtask. The most precise phrase is "in a later microtask", which is slightly more precise than "at some later time". All that said, I can see how "anything can happen" is not very useful.
It says "start" because a future may have more than one listener, and we only promise that the first one is notified immediately. Then that can trigger a whole slew of work, including completing other async functions, which triggers their listeners, before coming back to the second listener.
We can actually promise that it will be later. We may even promise that it schedules that microtask using
(It's not actually implemented that way, the future is not awaited inside the function, but outside, so an error ends up being the result of the function body, even if the We could say that this implementation choice is now a promise. Which means it's something you can depend on, but also something that we cannot optimize further in the future. And if we start using JavaScript promises for our futures, we may have to do extra work to make it work the same. (That's why we don't like making promises, they can get in the way of future optimizations.) |
In Alibaba's Flutter branch, we have made modifications to the DartVM. By altering the generation of the Await Stub, we first check whether the incoming parameter is a Future, then determine whether that Future has already been completed. If it has been completed, we synchronously return the value to the caller. This modification significantly reduces the number of scheduled microtasks, thereby improving performance. Importantly, it is transparent to developers. Many plugins use this pattern: they first check whether the cached value is null, and if it is, they call another asynchronous function to fetch the value. Otherwise, they directly return the cached value. |
Documentation for
Future.value
says:sdk/sdk/lib/async/future.dart
Lines 328 to 329 in 99919c6
However, the implementation calls
_Future<T>.immediate
sdk/sdk/lib/async/future.dart
Lines 348 to 349 in 99919c6
It schedules a microtask to complete the future:
sdk/sdk/lib/async/future_impl.dart
Lines 269 to 270 in 99919c6
sdk/sdk/lib/async/future_impl.dart
Lines 577 to 599 in 99919c6
sdk/sdk/lib/async/future_impl.dart
Lines 636 to 641 in 99919c6
Scheduling a microtask is slower than just completing future synchronously. It also doesn't fully follow the documented behavior (the created Future is not fully completed until the microtask runs).
A good example of users struggling with this behavior is a
SynchronousFuture
from Flutter:https://github.com/flutter/flutter/blob/1e14993c56f668afcd2175c7ae847599a8ec11ee/packages/flutter/lib/src/foundation/synchronous_future.dart#L7-L17
It looks like
SynchronousFuture
would not be needed ifFuture.value
would complete the Future synchronously (and maybeFuture.then
would also avoid scheduling an extra microtask for the completed future).In addition to
Future.value
, it would be nice to revise other places where the current Future implementation schedules extra unnecessary microtasks when value is already available / future is already completed.@lrhn @mkustermann @mraleph @Hixie
The text was updated successfully, but these errors were encountered: