1
votes

I'm writing a program in Java which uses genetic algorithm to split one set of numbers into two sets so their's sum is equal or as close to equal as possible. The program is supposed to append results of each iteration in TextArea. I didn't want the program to freeze while doing computations so I put the logic in another thread. To be more specific:

  1. I created class Runner which extends Task. BestSpecimen is my class and it is used here to print iteration number and list of best solution(s) found in this iteration. Also it updates ProgressBas but it's not that important. I did it by adding following line in my controller class: runnerProgressPB.progressProperty().bind(runner.progressProperty());
  2. I wanted to do the same with updating TextArea but it didn't work properly. The point was that it updated TextArea with current iteration but deleted results from previous iterations.
  3. I used different approach: in my controller class I added this piece of code: runner.valueProperty().addListener((observable, oldValue, newValue) -> { resultsTA.appendText(newValue + "\n"); });
  4. Simple, it is just listening for changes of ValueProperty in thread called runner. I update the ValueProperty after creating BestSpecimen object which happens in the middle of the iteration. However, here comes the problem - many iterations are missing from TextArea. Take a look at the screenshot. There are over 100 iterations missing at one point! Screenshot
  5. Well, what I think is happening? The thread runs so fast that TextArea can't update info on time. I put some Thread.sleep() inside the for loop which reduced number of missing iterations but the program runs terribly slow! I used Thread.sleep(20) and it took 5-10 minutes to finish and there were still some missing iterations. That's not the solution.
  6. What I want to achieve? I want the runner thread to suspend after setting ValueProperty and wait until TextArea is not updated. I think I should use wait() / notify() construction but dunno where exactly. I'm pretty new in thread things...
  7. Here's the main loop in call() method of my Runner class. Before it, there are only declarations of variables.

        while (!this.done) {
            for (int i = 0; i < this.iterations; i++) {
                current = this.population.get(i);
                theBestOnes = current.getTheBestSpecimen();
                bestCase = new BestSpecimen(i+1, theBestOnes);
                this.updateValue(bestCase);
                this.updateProgress(i, iterations);
                next = Population.createParentPopulationFromExistingOne(current);
                next.fillPopulation();
                this.population.add(this.population.size(), next);
            }
            done = true;
        }
        return null;
    
  8. By the way, there is also this nasty null (see the screenshot above) which is appended to the TextArea. Any ideas how I can get rid off it? The return statement is reqiured at the end of the method, and I can't return BestSpecimen object with no content.

1
All the updateXXX(...) methods are intended to set the value of a property, not to provide a stream of values. Consequently they may coalesce multiple calls to a single call (in particular this is likely to happen if you update the value multiple times between two adjacent frame renderings). You need to explicitly append to the text area on the FX application thread. (Note that a text area may not be the best option if your output is huge.) - James_D
How can I do it? I run my computations in another thread and I can't do it from here. I need to update my TextArea in every iteration, not only when the thread finishes. Also, I use TextArea justo for testing, will change it in future. - Patryk

1 Answers

0
votes

The updateXXX methods in Task do exactly what they say: they update the value of a property. The update is performed on the FX Application Thread. Since the intention is to provide a value which may be observed in the UI, if the value is changed multiple times between frame renderings, only the latest value at each rendering of a frame will actually be used. This is stated explicitly in the documentation:

Updates the value property. Calls to updateValue are coalesced and run later on the FX application thread, so calls to updateValue, even from the FX Application thread, may not necessarily result in immediate updates to this property, and intermediate values may be coalesced to save on event notifications.

The simplest solution is the following (though it may not perform well for reasons outlined below:

while (!this.done) {
    for (int i = 0; i < this.iterations; i++) {
        current = this.population.get(i);
        theBestOnes = current.getTheBestSpecimen();
        bestCase = new BestSpecimen(i+1, theBestOnes);

        final String text = bestCase.toString()"+\n";
        Platform.runLater(() -> resultsTA.appendText(text));

        this.updateProgress(i, iterations);
        next = Population.createParentPopulationFromExistingOne(current);
        next.fillPopulation();
        this.population.add(this.population.size(), next);
    }
    done = true;
}
return null;

and obviously get rid of the listener to the valueProperty.

The problem here is that you may end up scheduling many operations to the FX Application Thread, and may end up swamping it with so much work that it can't perform its usual tasks of repainting the UI and handling user events, etc.

Fixing this gets a bit tricky. One option is to use a BlockingQueue<String> to queue the updates, and an AnimationTimer to update the UI with them:

BlockingQueue<String> textQueue = new LinkedBlockingQueue<>();

// ...

int iterations = ... ;

Now in your task do

for (int i = 0; i < this.iterations; i++) {
    current = this.population.get(i);
    theBestOnes = current.getTheBestSpecimen();
    bestCase = new BestSpecimen(i+1, theBestOnes);

    final String text = bestCase.toString()+"\n";
    textQueue.put(text);

    this.updateProgress(i, iterations);
    next = Population.createParentPopulationFromExistingOne(current);
    next.fillPopulation();
    this.population.add(this.population.size(), next);
}
return null;

and when you start the task, also start an AnimationTimer as follows:

AnimationTimer timer = new AnimationTimer() {

    private int updates = 0 ;
    @Override
    public void handle(long now) {
        List<String> newStrings = new ArrayList<>();
        updates += textQueue.drainTo(newStrings);
        StringBuilder sb = new StringBuilder();
        newStrings.forEach(sb::append);
        resultsTA.appendText(sb.toString());
        if (updates >= iterations) {
            stop();
        }
    }
};

timer.play();

Even this might not perform very well, as might have a large amount of text (and lots of string concatenation to build it up). You could consider using a ListView instead of a TextArea, in which case you can just drain the queue directly to the list view's items.