multithreadingjavafxtask

javafx.concurrent.Task: How to separate library and GUI?


I have a JavaFX application where one functionality involves searching (large) binary data for specific patterns. To keep the GUI responsive, I am creating a concurrent task and put it on a differen thread:

public void search(SearchPattern pattern) {
    
    Task<ObservableList<...>> task = new Task<>() {
        @Override protected ObservableList<...> call() throws Exception {

            try {                    
                for(int i=0;i<....size();i++) {
                    if (isCancelled()) {
                        break;
                    }
                    if(i%50000 == 0) {
                        updateProgress(i, entries.size());
                    }
                    if(pattern.matches(entries.get(i))) {
                        // do stuff
                    }                        
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                // IO cleanup
            }
            return FXCollections.observableArrayList(...);
        }
    };

    progressBar.progressProperty().bind(task.progressProperty());

    task.setOnSucceeded(e -> {
        unregisterRunningTask(task);
        searchResults = task.getValue();
    });

    Thread thread = new Thread(task);
    registerRunningTask(task);
    thread.setDaemon(false);
    thread.start();
}

This works fine, however mingles (library) functionality with GUI code. In particular I would like to put the search code into a separate library, write test-cases for that functionality - completely automated, without a GUI - and of course when testing from the commandline, a separate thread is not needed. Right now, I am limited to putting pattern.matches(entries.get(i)) in that library. A cleaner way would be to have a function

searchpattern(filename) {
    for(int=0;i< ... ;i++) {
       ...
    }
    return observableArrayList(...);
}

in the library that includes the complete loop, write my test-cases and benchmarks on that, and then somehow involve that function in the GUI code. The problem I face when separating this code is:

updateProgress() requires a reference to a task. Of course I could always create a task when trying to implement test-cases, i.e. even when invoking searchpattern(filename) from a non-GUI context. I could simply omit any progress updates in my code, but that creates a very bad user-experience.

Is there any other way to pass updates from that (potentially) long-running function using some abstraction layer, that makes this function easy in a library both from a (threaded) GUI context and a non-threaded commandline context?


Solution

  • Here are two approaches. To make things more concrete, suppose we have a class that implements a potentially long-running process on a list:

    public abstract class ListProcessor<T> {
    
        private final List<T> items ;
    
        public ListProcessor(List<T> items) {
            this.items = items ;
        }
    
        public void process() {
            for (int i = 0 ; i < items.size(); i++) {
                processItem(items.get(i));
            }
        }
    
        public abstract processItem(T item);
    }
    

    (I'm making a ton of assumptions here about the list not being modified during processing, etc.)

    The first approach is as described by @Slaw in a comment. In this approach, your external library class keeps a list of listeners that are notified when the number of items processed changes. You could simply use a DoubleConsumer for this. For example:

    public abstract class ListProcessor<T> {
    
        private final List<T> items ;
    
        private final List<DoubleConsumer> listeners = new ArrayList<>();
    
        public ListProcessor(List<T> items) {
            this.items = items ;
        }
    
        public void addListener(DoubleConsumer listener) {
            listeners.add(listener);
        }
    
        public void removeListener(DoubleConsumer listener) {
            listeners.remove(listener);
        }
    
        public void process() {
            int n = items.size();
            for (int i = 0 ; i < n; i++) {
                processItem(items.get(i));
                listeners.forEach(l -> l.accept(1.0 * i / n));
            }
        }
    
        public abstract processItem(T item);
    }
    

    You can use this in a task as follows:

    List<Widget> widgets = ... ;
    ListProcessor<Widget> widgetProcessor = new WidgetProcessor(widgets);
    
    Task<Void> widgetTask = new Task<>() {
        @Override
        protected Void call() {
            DoubleConsumer progressUpdater = prog -> updateProgress(prog, 1.0);
            widgetProcessor.addListener(progressUpdater);
            widgetProcessor.process();
            widgetProcessor.removeListener(progressUpdater);
            updateProgress(1.0, 1.0);
            return null;
        }
    };
    
    progressBar.progressProperty().bind(widgetTask.progressProperty());
    // ...
    executor.execute(widgetTask);
    

    This relies on your library class implementing a listener-notification mechanism. If you don't want to do that, or if you are using some third-party library that doesn't implement it, you can poll the processing object for its current progress. I would do the polling from an AnimationTimer, though other approaches are possible. The minimum requirement (as far as I can tell) is that the processing object has a way of reporting its progress in a thread-safe manner (as you will be polling it from a different thread to the one that is updating it).

    So, for example:

    public abstract class ListProcessor<T> {
    
        private final List<T> items ;
    
        private volatile double progress = 0.0;
    
        public ListProcessor(List<T> items) {
            this.items = items ;
        }
    
        public void process() {
            int n = items.size();
            for (int i = 0 ; i < n; i++) {
                processItem(items.get(i));
                progress = (1.0 * i / n);
            }
        }
    
        public double getProgress() {
            return progress;
        }
    
        public abstract processItem(T item);
    }
    

    And then

    List<Widget> widgets = ... ;
    ListProcessor<Widget> widgetProcessor = new WidgetProcessor(widgets);
    
    Task<Void> widgetTask = new Task<>() {
        @Override
        protected Void call() {
            AnimationTimer progressUpdater = new AnimationTimer() {
                @Override
                public void handle(long now) {
                    updateProgress(widgetProcessor.getProgress(), 1.0);
                }
            };
            progressUpdater.start();
            widgetProcessor.process();
            progressUpdater.stop();
            updateProgress(1.0, 1.0);
            return null;
        }
    };
    
    progressBar.progressProperty().bind(widgetTask.progressProperty());
    // ...
    executor.execute(widgetTask);
    

    In both cases, you should make sure you implement proper clean-up in the case of exceptions being thrown, etc. (It's probably better to do the clean-up in a state listener or in the succeeded() and failed() methods.)