Skip to content

Isolates are way too complex for a standard user #50456

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

Open
bernaferrari opened this issue Nov 14, 2022 · 12 comments
Open

Isolates are way too complex for a standard user #50456

bernaferrari opened this issue Nov 14, 2022 · 12 comments
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-isolate

Comments

@bernaferrari
Copy link
Contributor

I've been trying to use Dart Isolates for the past few days and it's been a total nightmare. Most of the documentation and tutorials ask to use compute from Flutter. But compute returns the result, not the context, so it is not cancellable. Then, the function that you pass must be in the root level, not in the function level, which makes the error message unclear:

[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument(s): Illegal argument in isolate message: (object extends NativeWrapper - Library:'dart:ui' Class: Path)

I heard there is also a limitation where you can't return every type, like only primitive types work. And that it copies the values when returning. So, making a simple Isolate from hand is really really hard, there are many edge scenarios, and sometimes it is slower. It is done in a way no one should use, except most people could benefit from parallelism.

I understand how it is thread-safe, memory-safe, and these things are great. But it is implemented in such a non-intuitive way that no uses it in normal situations.

I wish Dart could learn some lessons from coroutines or goroutines and make "async"/multi-thread/parallelism easy to use for anyone. A few months ago, there was describeEnum in Flutter, now Dart takes care of it. Now, I wish there were some care for compute, so hopefully one day it can be deprecated in Flutter with a better solution.

How it is right now (got this code somewhere on stack overflow):

Future<Person> fetchUser() async {
      ReceivePort port = ReceivePort();
      String userData = await Api.getUser();
      final isolate = await Isolate.spawn<List<dynamic>>(
          deserializePerson, [port.sendPort, userData]);
      final person = await port.first;
      isolate.kill(priority: Isolate.immediate);
      return person;
}

void deserializePerson(List<dynamic> values) {
    SendPort sendPort = values[0];
    String data = values[1];
    Map<String, dynamic> dataMap = jsonDecode(data);
    sendPort.send(Person(dataMap["name"]));
}

How it could be:

Future<Person> fetchUser() async {
    final person = await launch(Thread.io, () => deserializePerson(userData));
}

The scoping mechanism with launch and useContext in Kotlin is perfect to me. It is not memory safe, but the scope helps it be easy to use and intuitive.

@modulovalue
Copy link
Contributor

Unfortunately, this is a very complex topic where it is extremely hard to hide the underlying complexity without making Isolates significantly less useful.

There is an excellent package on pub.dev that I'd recommend which is actors. If it doesn't serve your use cases, then I'd highly recommend for you to take a look inside of it. It is well tested and can serve as a good starting point for making isolates work with your use case.

Note: Dart 2.19 comes with Isolate.run See: #40238

@bernaferrari
Copy link
Contributor Author

Can you cancel Isolate.run? Or it is just compute with another name?

@modulovalue
Copy link
Contributor

I don't think so.
Yes, I believe so, i.e. it can used to move a computation to another isolate and get it back asynchronously without blocking the current isolate.

@vsmenon vsmenon added library-isolate area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. labels Nov 14, 2022
@MaryaBelanger
Copy link
Contributor

MaryaBelanger commented Aug 23, 2023

Hi @bernaferrari, I'm wondering if you've had a chance to use Isolate.run yet?

I think you'd really enjoy this blog post, "Better isolate management with Isolate.run()". It explains how run works specifically to address some of the issues you described (it even begins by going into detail on just how much of a nightmare using isolates is without run 🙂):

Most of the documentation and tutorials ask to use compute from Flutter.

I'm not too familiar with Flutter, but I know compute now uses run instead of spawn (so presumably some of the benefits carry over)

Then, the function that you pass must be in the root level, not in the function level,

run allows sending closures, and it abstracts the use of sendPort and responsePort, so you can pass it any function without having to explicitly configure those values.

I heard there is also a limitation where you can't return every type, like only primitive types work.

More kinds of objects are now allowed to be sent between isolates (see #46623, and send API docs for the types)

And that it copies the values when returning.

run transfers memory between isolates using Isolate.exit(), it doesn't copy.

So, making a simple Isolate from hand is really really hard, there are many edge scenarios, and sometimes it is slower.

You no longer need to build out any sendPort or responsePort like with spawn, run encapsulates all of that complexity. It basically looks exactly how you described:

    final person = await Isolate.run(() => deserializePerson(userData));
    // not an exact recreation but you get the idea!

Plus run has built in error handling that helps reduce complexity for edge scenarios.

Isolate.spawn absolutely was unintuitive, which the blog post goes into detail about, so I think you'll really enjoy reading it. The dart.dev documentation was also updated to more succinctly show how to use run .

Let us know if that's helpful!

@bernaferrari
Copy link
Contributor Author

I will answer better later, but I don't like Isolate.run because I can't cancel it.

@MaryaBelanger
Copy link
Contributor

For better visibility you might want to create a separate issue specifically describing the cancelling functionality you'd like to see for isolates (and maybe close this one if you agree that most of the general complexity of isolates is addressed by run)

@mkustermann
Copy link
Member

I will answer better later, but I don't like Isolate.run because I can't cancel it.

Our APIs currently don't allow cancelling synchronous or asynchronous operations - independent of isolates. One could make this cancellable with semantics of just killing the helper isolate. That is a little tricky because our Futures aren't cancellable atm. But some team members are thinking about how we could add cancellable futures (/cc @mraleph )

@bernaferrari
Copy link
Contributor Author

Yeah, for me isolates are useless because what if the user starts a long running async operation and navigates to different screen? I need a way to stop that. If I don't use isolate, the screen freezes. So I don't have an option right now.

@modulovalue
Copy link
Contributor

@bernaferrari as I said in #50456 (comment), have you considered looking at actors?

Note: there are some primitives for cancelable operations in package:async such as CancelableOperation and CancelableCompleter

@bernaferrari
Copy link
Contributor Author

I took a look, but it is still feels too complex, most of isolates seem to be targeted into a "execute a long running operation, interact from the outside, get a result", it fits more the context of a super server than an app. For me, Kotlin corutines are perfect, I can scope into where I am, initialize/kill with easy, select which thread I want (I think Android has 4 by default which are enough). It never overwhelms, it is always just enough. I can also run coroutines out of functions, like myfunction = launch { }, so with the launch it is guaranteed to be in another thread, and it dies automatically if the current screen context is disposed. I can't wrap a function with isolates, and I can't do in a "responsive react/flutter-like way", everything is still too "start, do this, wait for that, iterate on that, finish". I miss this, a lot. And I think it is not me, because isolates are barely used right now, where in Android coroutines are used everywhere, because they are super simple to use.

@modulovalue
Copy link
Contributor

@bernaferrari It looks to me like you are looking for a feature that looks and feels like coroutines in Kotlin, is that correct?

Note that coroutines are not strictly better than Isolates, because you can run into synchronization issues while using them. Such synchronization issues are hard to detect and can be introduced by accident.

Isolates are fundamentally a different feature. They allow you to add true concurrency to your app without also introducing any synchronization issues by accident. This comes at the cost of some convenience, but you won't have any accidental synchronization issues!

I agree with the following comment by MaryaBelanger #50456 (comment), that is, it would probably be best to describe any issues you have with Isolate.run (and the fact that you can't cancel them) in a new issue.

@bernaferrari
Copy link
Contributor Author

bernaferrari commented Aug 23, 2023

I'm looking for a way that Isolates could be useful for a standard user, people are afraid right now because it is too hard to use. Kotlin is an example, but Go or Swift could also work well for Dart. Even Java, with threads, can have dozens of issues, but is easier to use than Dart right now.

I agree with the synchronisation issues, but my biggest issue right now is that I can't use isolates in a consistent regular way, neither anyone. My theory is that it is because it is too hard. Even Flutter's official color palette example (which uses isolates) has bugs because there is no way to cancel it, so two isolates happen at the same time and finish in the wrong order.

I have issues such as, not being able to invoke a function in an isolate without any compiler warning, only runtime warning, because process.io is not static but since it is being imported from another library, Dart ignores it.

For me, the ideal feature would be an Isolate.run that is cancelable, allows to select the thread from a poll (and queues in that poll), allows non static functions (that comes at a cost, I know, but right now it is too complex). Most of all, I miss an Isolates example that is like 2 LOC, and not 15 to do something trivial. The "send"/"receive" listener mechanism is the biggest trouble I have. I would prefer to have a variable or callback that listens to updates, so random numbers don't need to be introduced.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-isolate
Projects
None yet
Development

No branches or pull requests

5 participants