-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Allow asynchronous code in a synchronous context #31102
Comments
@floitschG Isn't this first and foremost a language/library discussion. Without knowing whether and if so, what, mechanism to expose there is no VM work to do. |
I feel the pain. Futures and explicit asynchrony is a solution to a problem that works on all the platforms that we target. It's not necessarily the best solution, but it works even when the only primitive you have is a single thread and a top-level event loop. This request can be approached from many directions.
As a language designer, I don't see language changes in this direction happening any time soon. Compiling to JavaScript is hard enough already. As a core platform library designer, I don't think any isolate-blocking operations are likely either. Again, compiling to JavaScript, which doesn't have any good way to block the current execution better than This leaves the platform specific solution. If the stand-alone VM wants to provide such functionality, then I'm not opposed to it, and wouldn't mind helping with the design, but it would mean that any code using the feature will be locked to that one platform. That does diminish the value of the feature significantly. |
FWIW, I primarily work on Dart web products, and I don't care if this doesn't work in JavaScript. The idea of trying to only maintain and create language features and support them iff the browser can support them has already has thrown out the window. We don't support In fact, we've had multiple "experimental" language features (assert in initializers, super mixins) that haven't worked in any web product for probably over a year already, and I've yet to see any real issue. The up-and-coming most popular cross-platform languages most similar to Dart (C#, Kotlin, Swift) don't care if every language feature works on every platform in every possible configuration. I'd hope that isn't a blocker for Dart either. |
That is the point I'm trying to make: We have platform specific (or multi-but-not-all-platform) features already (in Then it's a matter of discussing the viability of the features with the platforms that actually support that library, and deciding whether it's useful enough that we want it, and not something we'll regret later because it gets used so much that a lot of useful libraries become platform specific. A blocking operation seems like it would fit in abstract class BlockingReceivePort {
/// Port for sending events to this receive port.
SendPort get sendPort;
/// Close the [BlockingReceivePort].
/// No further events are received and further calls to [next] will throw.
void close();
/// Receive the next received object that has been sent on [sendPort].
/// Blocks the entire isolate until an object is received.
/// The blocking receive port will buffer events received before [next] is called and
/// deliver them immediately instead of blocking.
Object next();
} You can call Blocking an isolate is a powerful feature. It's unclear whether the isolate can receive other port events, even control port events, while blocked (it'll probably be the same as if it's blocked on a an OS call). In any case, it's not a feature that is planned for Dart 2. |
To be clear, I'm interested in this as a VM-only feature. I'm pretty sure that it's not just difficult to compile to JS, it's actually impossible (at least as long as we support interop). We could talk about some way to write code that's polymorphic over asynchrony, but that's out of scope for what I'm requesting here. Unless I'm missing something, I don't think the |
If you need to run something in the same isolate while blocking other synchronous code, then yes, you need multiple stacks (as co-routines or fibers or something else). I'm guessing that is significantly harder to implement than plain isolate blocking - but I could be wrong. If the VM is using threads internally anyway, it might be able to do something with those, even if only one of them is running at a time. It's definitely a VM-only feature then. |
If we ever go with Web Assembly the whole "VM-only" world disappears. Just saying 😄 |
I don't think threads are necessary; for example, Ruby doesn't support OS threads, but it does support fibers. All that's really necessary is an internal representation of the stack. My |
The
which the There are things you can do by rerunning the event loop code on top of the "blocked" stack, see, e.g., the Java Foxtrot library. It has obvious limits, at least you have to maintain a stack discipline for the blocked operations. There's also the option of stack rewriting, copying the stack away when blocking, reinstating it on release, which has to play well with both garbage collection and on-stack-replacement optimizations. But I'm guessing, I'm sure the VM engineers have a much better idea of what is needed and possible. |
The distinction is that I chose /// Runs the event loop until [test] returns `false`.
void yieldWhile(bool test()); This could be implemented in terms of void yieldWhile(bool test()) {
do {
blockUntilMessage();
} while (test());
}
void blockUntilMessage() => yieldWhile(() => false);
Yeah, this is the benefit of
Yep, this is the sort of thing that's necessary to implement full fibers. Fibers are very powerful, and I think it may well ultimately be worth the effort, but I'd be happy with a simpler solution for now. |
Most likely that won't work. The problem is that the isolate receiving a port message is not the same as that message being delivered to the receive port handler. That is, you don't get any visible effect from blocking because the state afterwards is exactly the same as the state before blocking. You need to run Dart code to change that ... unless you have some platform supported construct that changes state due to receiving the event. That's what the The |
The idea is that |
Indeed, that would be necessary, bringing us back to the strategy of running the event loop on top of the stack. It might only be a single event being delivered, but I'll wager that you'd also want any microtasks triggered by that event to run as well. Even with that, it's strictly simpler than running the full event loop while waiting for the block to complete. All in all, probably doable. It really amounts to blocking until the next event loop event, then executing that on top of the current stack, then continuing execution of that stack. That's why it requires platform support (there is no way to access the JS event queue except returning to it). Event loop events are not just port messages, they can also be timers (non-zero timers are port messages in the VM too, zero-duration timers are not) and possibly also other things like I/O. You might want to unblock for any event (blocking for an asynchronous HTTP fetch to complete is likely to require many events, which may or may not be implemented as port messages). With that in mind, it's probably simpler to just run the entire event loop on top of the stack, until some condition is true. Like a future having completed (which it can then do). T waitFor<T>(Future<T> future) {
Result<T> result = null;
Result.capture(future).then((r) { result = r; });
// Checks the condition between top-level events.
// Saves and restores current microtask queue.
_internalRunEventLoop(until: () => result != null);
if (result.isValue) return result.asValue.value;
// rethrow the async error synchronously.
_internalRethrow(result.asError.error, result.asError.stackTrace);
} Waiting for port messages seems less general. (I'm not saying that we want this, just that it's probably doable, with some, likely significant, amount of refactoring of the event loop code to make it reentrant). |
I don't think we'd need to handle microtasks any differently than any other event; I'd expect |
Sorry for not following along with all the discussion in this issue. Please see the below CL for a primitive from the VM that I think would help with this use-case. It's not perfect, and is still being debated by the VM team, but PTAL: https://dart-review.googlesource.com/#/c/sdk/+/25281/ |
@zanderso That looks great! It looks like that would solve this use case. In the CL, you say that the analyzer should complain about this in a lot of configurations. Under what configurations is |
Any embedder that is providing its own message loop will probably become wedged on a call to |
Is there a path for platforms like that to become compatible with |
Even if there was, suspending the UI thread to wait for anything would cause jank. |
I'm not sure I follow—doesn't the proposed function only suspend execution until there's an event available to handle, meaning that UI actions would still be dispatched as quickly as they would otherwise? After thinking about the proposed semantics, I'm curious how you'd use it to implement the T waitFor<T>(Future<T> future) {
Result<T> result;
Result.capture(future).then((r) => result = r);
while (result == null) {
waitForEventSync();
}
if (result.isValue) return result.asValue.value;
_internalRethrow(result.asError.error, result.asError.stackTrace);
} But what happens if |
I do think that there are various ways that the Flutter event loop could be rearchitected to achieve this, however there are no simple tweaks to the way that Flutter works today that can do this. The simple tweaks would all result in the delayed handling of requests to run the Right, you'd need something like: T waitFor<T>(Future<T> future) {
Result<T> result;
Result.capture(future).then((r) => result = r);
Timer.run(() {}); // Ensure that there is at least one message.
while (result == null) {
waitForEventSync();
}
if (result.isValue) return result.asValue.value;
_internalRethrow(result.asError.error, result.asError.stackTrace);
} |
This seems like a big pitfall, especially considering that the failure case is a deadlock that may or may not occur depending on the context of the call. If these semantics are necessary, maybe it would be better to keep |
A few things:
|
Can we set up a VC to talk about this? |
Side question. What is |
https://www.dartdocs.org/documentation/async/1.13.3/async/Result-class.html |
The issue is closed by landing the waitForEventSync implementation on the VM: Reland: [dart:io] Adds waitForEventSync The only fix needed for relanding is adding _ensureScheduleImmediate Original commit message: Adds a top-level call waitForEventSync to dart:io that blocks the |
"waitForEventSync" doesn't show up when I search the CHANGELOG. Should we consider this complete if we haven't documented it in the changelog? |
CHANGELOG update landed here: https://dart-review.googlesource.com/#/c/sdk/+/29041/ |
Asynchronous code in Dart is currently contagious, in the sense that as soon as one portion of an API is asynchronous, every caller must be made asynchronous. We work around this in various ways, including gradually adding more and more synchronous versions of built-in asynchronous APIs, but splitting every API into two versions quickly becomes untenable as code gets more complex and has more layers.
We're running into this issue with Dart Sass today. All of Sass's compilation logic is purely synchronous, and we need to provide the ability for people to invoke it synchronously. However, it also provides extension points to allow users to write custom functions and importers, and in some cases these need to do work that's only possible asynchronously. For example, to integrate it with the
build
package, it needs to be able to read inputs using the asynchronousAssetReader.readAsString()
method.In the server-side JavaScript world, the
fibers
package provides coroutine functionality. The Dart VM could provide something similar that would be one way to make this work without violating the principle of having only one active thread of execution per isolate.A simpler but less general possibility would be to provide a
blockUntilMessage()
function that blocks the current isolate until a port has an incoming message, handles that message by invoking its Dart handler, and returns. This could be used as a primitive building block for more user-friendly synchronization functions:This is effectively a form of coroutines in which the execution can only be suspended and resumed in a very restricted way.
The text was updated successfully, but these errors were encountered: