2

I have an app uploading joystick position data to a webserver using an API call.

This method gets called when a joystick is moved. It stops any previously running isolate and starts a new isolate if the joystick is not in the centre.

void onJoystickMoved(double angle, double distance) {
stopIsolate();
if(distance > 0.06){
  startIsolate(JoystickPosition.fromDistanceAndRadius(distance, angle));
}
}

The isolate start and stop methods

Future<void> startIsolate(JoystickPosition position) async {
   isolate = await Isolate.spawn(uploadJoystickPosition, position);
}

void stopIsolate() {
   if (isolate != null) {
     debugPrint("Stopping isolate");
     isolate.kill();
     isolate = null;
   }
}

uploadJoystickPosition method (the method in the isolate):

void uploadJoystickPosition(JoystickPosition position){

   Timer.periodic(new Duration(seconds: 1), (Timer t) {
      DataModel dataModel = DataModel(1, getTimeInSeconds());
      dataModel.joystickPosition = position;
      debugPrint("Distance: ${position.distance}");
      uploadData(dataModel).then(uploadResponse, onError: uploadError);
   });
}

Trouble is the uploadJoystickPosition keeps uploading old positions of the joystick along with new positions. I am assuming this is because the timer keeps running even when the isolate is killed.

Questions:

  1. Why does my timer keep going(and uploading) even after I kill the isolate?
  2. How do I get my timer to stop when I kill the isolate its running in?
DrkStr
  • 1,752
  • 5
  • 38
  • 90
  • 1
    Can you provide a minimal, complete, verifiable example? I can't reproduce your problem when running https://gist.github.com/jamesderlin/29b5b34608d851531e7161703e86ccf4 with the Dart VM on Linux. – jamesdlin Aug 13 '20 at 21:20
  • I've never worked on Dart on its own, and I needed this to work on flutter. So I made a bare-bones flutter app. https://github.com/Drkstr/flutter_isolate_test. Clicking "Start Isolate" should start an isolate that will repeatedly print the time the isolate was started on the console. Clicking "Start Isolate" again should kill the previous isolate and start a new one. This works fine if you click on "Start Isolate" wait for a few seconds and then click on "Start Isolate" again. It fails to kill the previous isolate if you repeatedly mash the "Start Isolate" in quick succession. – DrkStr Aug 14 '20 at 13:22
  • Well, that's a different problem then, isn't it? The issue then is not that killing an isolate doesn't cancel the `Timer` but that you have an isolate that you can't kill. And since it works normally but doesn't when you hit the "Start Isolate" button in quick succession, it sounds like you have a race condition. Glancing at your code, there doesn't seem to be anything that prevents calling `startIsolate` while another call to `startIsolate` is already in progress. You therefore would end up leaking isolates. – jamesdlin Aug 14 '20 at 14:55
  • Your right. The issue is not with the Timer as I originally taught, but with the isolate that isn't getting killed. The question then becomes, how do I ensure that the last isolate is killed off before a new one is started. The isolate.kill() method doesn't return anything, so I can't check to see if it has been killed or not. Also, am I better off deleting this question and posting a new one? – DrkStr Aug 15 '20 at 13:22
  • 1
    I don't know of a good way to tell when an isolate is killed. In your example code, you don't really need to know; it should be sufficient to make `startIsolate` do nothing if `isolate != null`. That would prevent creating an isolate while another creation request is already in progress. – jamesdlin Aug 15 '20 at 20:59
  • There is a chance that the new isolate would never start if I use the condition "do nothing if isolate != null". Is there some way I can wait for the previous isolate to be null before I start a new isolate? – DrkStr Aug 16 '20 at 02:27
  • 1
    My previous comment wasn't quite correct; you can't check `isolate != null` since `isolate` wouldn't be set until after the `Future` completes. See [my answer](https://stackoverflow.com/a/63433061/) below. – jamesdlin Aug 16 '20 at 04:12

2 Answers2

2

As I noted in a comment, your example code has:

Future<void> startIsolate() async {
  stopIsolate();
  isolate =
      await Isolate.spawn(isolateMethod, DateTime.now().toIso8601String());
}

void stopIsolate() {
  if (isolate != null) {
    debugPrint("Stopping isolate");
    isolate.kill();
    isolate = null;
  }
}

Nothing stops startIsolate from being called while another call to startIsolate is already in progress. Therefore your problem is not that killing an isolate doesn't stop its Timers, it's that you leak isolates and prevent yourself from killing them. You need to add a guard to avoid spawning a new isolate while another creation request is in progress. A bool would be sufficient:

bool isStartingIsolate = false;

Future<void> startIsolate() async {
  if (isStartingIsolate) {
    // An isolate is already being spawned; no need to do anything.
    return;
  }

  stopIsolate();

  isStartingIsolate = true;
  try {
    isolate =
        await Isolate.spawn(isolateMethod, DateTime.now().toIso8601String());
  } finally {
    isStartingIsolate = false;
  }
}

A different approach, if you want to wait for the pending startIsolate call to finish before processing any new ones:

Future<void> pendingStartIsolate;

Future<void> startIsolate() async {
  while (pendingStartIsolate != null) {
    await pendingStartIsolate;
  }

  stopIsolate();

  try {
    pendingStartIsolate =
        Isolate.spawn(isolateMethod, DateTime.now().toIso8601String());
    isolate = await pendingStartIsolate;
  } finally {
    pendingStartIsolate = null;
  }
}
jamesdlin
  • 81,374
  • 13
  • 159
  • 204
0

Check out the plugin easy_isolate, it provides an easy way to use isolates with safe checks already done and other cool features, also with a well explained documentation.

https://pub.dev/packages/easy_isolate

Lucas Britto
  • 355
  • 1
  • 8