How to stop class/functions from continuing to exe

2019-08-21 08:46发布

问题:

I have made a few questions about this already, but maybe this question would result in better answers(i'm bad at questions)

I have one class, called FOO, where I call an async Start function, that starts the process that the class FOO was made to do. This FOO class does a lot of different calculations, as well as posting/getting the calculations using the node.js "requets" module.

-I'm using electron UI's (by pressing buttons, that executes a function etc..) to create and Start the FOO class-

class FOO {
  async Start(){
    console.log("Start")
    await this.GetCalculations();
    await this.PostResults()
  }
  
  async PostResults(){
    //REQUESTS STUFF

    const response = {statusCode: 200} //Request including this.Cal
    console.log(response)
    //Send with IPC
    
    //ipc.send("status", response.statusCode)
  }
  
  async GetCalculations(){
    for(var i = 0; i < 10; i++){
      await this.GetCalculation()
    }

    console.log(this.Cal)
  }

  async GetCalculation(){
    //REQUEST STUFF
    const response = {body: "This is a calculation"} //Since request module cant be used in here.

    if(!this.Cal) this.Cal = [];

    this.Cal.push(response)
  }
}
        
var F1 = new FOO();
        
F1.Start();

Now imagine this code but with A LOT more steps and more requests ect. where it might take seconds/minutes to finish all tasks in the class FOO.

-Electron got a stop button that the user can hit when he wants the calculations to stop-

How would I go about stopping the entire class from continuing?

In some cases, the user might stop and start right after, so I have been trying to figure out a way to STOP the code from running entirely, but where the user would still be able to create a new class and start that, without the other class running in the background.

I have been thinking about "tiny-worker" module, but on the creation of the worker, it takes 1-2 seconds, and this decreases the purpose of a fast calculation program.

Hopefully, this question is better than the other ones.

Update:

Applying the logic behind the different answers I came up with this:

await Promise.race([this.cancelDeferred, new Promise( async (res, req) => {
        var options ={
            uri: "http://httpstat.us/200?sleep=5000"
        }

        const response = await request(options);

        console.log(response.statusCode)

    })])

But even when the

this.cancelDeferred.reject(new Error("User Stop"));

Is called, the response from the request "statuscode" still gets printed out when the request is finished.

The answares I got, shows some good logic, that I didn't know about, but the problem is that they all only stop the request, the code hanlding the request response will still execute, and in some cases trigger a new request. This means that I have to spam the Stop function until it fully stops it.

回答1:

Framing the problem as a whole bunch of function calls that make serialized asynchronous operations and you want the user to be able to hit a Cancel/Stop button and cause the chain of asynchronous operations to abort (e.g. stop doing any more and bail on getting whatever eventual result it was trying to get).

There are several schemes I can think of.

1. Each operation checks some state property. You make these operations all part of some object that has a aborted state property. The code for every single asynchronous operation must check that state property after it completes. The Cancel/Stop button can be hooked up to set this state variable. When the current asynchronous operation finishes, it will abort the rest of the operation. If you are using promises for sequencing your operations (which it appears you are), then you can reject the current promise causing the whole chain to abort.

2. Create some async wrapper function that incorporates the cancel state for you automatically. If all your actual asynchronous operations are of some small group of operations (such as all using the request module), then you can create a wrapper function around whichever request operations you use that when any operation completes, it checks the state variable for you or merges it into the returned promise and if it has been stopped, it rejects the returned promise which causes the whole promise chain to abort. This has the advantage that you only have to do the if checks in one place and the rest of your code just switches to using your wrapped version of the request function instead of the regular one.

3. Put all the async steps/logic into another process that you can kill. This seems (to me) like using a sledge hammer for a small problem, but you could launch a child_process (which can also be a node.js program) to do your multi-step async operations and when the user presses stop/cancel, then you just kill the child process. Your code that is monitoring the child_process and waiting for a result will either get a final result or an indication that it was stopped. You probably want to use an actual process here rather than worker threads so you get a full and complete abort and so all memory and other resources used by that process gets properly reclaimed.

Please note that none of these solutions use any sort of infinite loop or polling loop.


For example, suppose your actual asynchronous operation was using the request() module.

You could define a high scoped promise that gets rejected if the user clicks the cancel/stop button:

function Deferred() {
    let p = this.promise = new Promise((resolve, reject) => {
        this.resolve = resolve;
        this.reject = reject;
    });
    this.then = this.promise.then.bind(p);
    this.catch = this.promise.catch.bind(p);
    this.finally = this.promise.finally.bind(p);
}    

// higher scoped variable that persists
let cancelDeferred = new Deferred();

// function that gets called when stop button is hit
function stop() {
   // reject the current deferred which will cause 
   //    existing operations to cancel
   cancelDeferred.reject(new Error("User Stop"));

   // put a new deferred in place for future operations
   cancelDeferred = new Deferred();
}

const rp = require('request-promise');

// wrapper around request-promise
function rpWrap(options) {
     return Promise.race([cancelDeferred, rp(options)]);
}

Then, you just call rpWrap() everywhere instead of calling rp() and it will automatically reject if the stop button is hit. You need to then code your asynchronous logic so that if any reject, it will abort (which is generally the default and automatic behavior for promises anywa).



回答2:

Asynchronous functions do not run code in a separate thread, they just encapsulate an asynchronous control flow in syntactic sugar and return an object that represents its completion state (i.e. pending / resolved / rejected).

The reason for making this distinction is that once you start the control flow by calling the async function, it must continue until completion, or until the first uncaught error.

If you want to be able to cancel it, you must declare a status flag and check it at all or some sequence points, i.e. before an await expression, and return early (or throw) if the flag is set. There are three ways to do this.

  • You can provide a cancel() function to the caller which will be able set the status.
  • You can accept an isCancelled() function from the caller which will return the status, or conditionally throw based on the status.
  • You can accept a function that returns a Promise which will throw when cancellation is requested, then at each of your sequence points, change await yourAsyncFunction(); to await Promise.race([cancellationPromise, yourAsyncFunction()]);

Below is an example of the last approach.

async function delay (ms, cancellationPromise) {
  return Promise.race([
    cancellationPromise,
    new Promise(resolve => {
      setTimeout(resolve, ms);
    })
  ]);
}

function cancellation () {
  const token = {};

  token.promise = new Promise((_, reject) => {
    token.cancel = () => reject(new Error('cancelled'));
  });

  return token;
}

const myCancellation = cancellation(); 

delay(500, myCancellation.promise).then(() => {
  console.log('finished');
}).catch(error => {
  console.log(error.message);
});

setTimeout(myCancellation.cancel, Math.random() * 1000);