Correctly Executing Multithreading and Thread Pools with JavaFX Tasks

I have the ability for users to send multiple files from FileChooser to be processed by some code. The result would be an IO to read the file and then the actual heavy computation of the stored data. The user is allowed to select multiple files, and since file processing is independent of other selected files, it makes it easier for me to work with streams.

In addition, the user needs to have a list of buttons, one for each task to cancel, and a Cancel All button. Therefore, I must consider the possibility of selectively or collectively eliminating one or all of the tasks.

The last requirement is that I am not allowing the user to throttle the system by opening a ton of files. So I am considering a thread pool with a limited number of threads (let's pretend I will close it to 4 for some arbitrary number).

I'm not sure how to set this up correctly. I have the logic of what I need to do, but using the correct classes is where I am stuck.

I have already checked this resource , so if the answer is somehow there then I misread the article.

  • Are there JavaFX classes that can help me with this situation?

  • If not, how could I mix the task with some kind of thread pool? Should I create my own thread pool, or is there one already provided for me?

  • Can I make a singleton somewhere that contains the maximum number of threads I want to allow the user?

I would prefer to use it already in the Java library as I am not a multithreading expert and I am worried that I might get it wrong. Since thread errors are actually the worst on the planet to debug, I try very hard to make sure I do it as best I can.

If there is no way to do this and I need to flip my own implementation, what's the best way to do it?

EDIT: I should point out that I am generally new to streams, I have used them before and I read books on them, but this will be my first main use of them and I would really like to get it right.

+3


source to share


1 answer


JavaFX has an API javafx.concurrent

; in particular, the class Task

fits your use case very well. This API is designed to work with the API java.util.concurrent

. For example, it Task

is an implementation FutureTask

, so it can be sent to Executor

. Since you want to use a thread pool, you can create Executor

one that implements a thread pool for you and send your tasks to it:

final int MAX_THREADS = 4 ;

Executor exec = Executors.newFixedThreadPool(MAX_THREADS);

      

Since these threads run in the background of the UI application, you probably don't want them to prevent the application from shutting down. You can achieve this by creating threads created by your executor daemon threads:

Executor exec = Executors.newFixedThreadPool(MAX_THREADS, runnable -> {
    Thread t = new Thread(runnable);
    t.setDaemon(true);
    return t ;
});

      

The resulting executor will have a pool of up to MAX_THREADS

threads. If tasks are submitted when threads are not available, they will wait in the queue until the thread is available.

To implement the actual one Task

, there are a few things to keep in mind:



You shouldn't update the UI from a background thread. Since yours is Task

dispatched to the executor above, the method call()

will be called on a background thread. If you really need to change the UI during method execution call

, you can wrap the code that changes the UI in Platform.runLater(...)

, but better structure things so you avoid this situation. In particular, it Task

has a set of methods updateXXX(...)

that change the values โ€‹โ€‹of the corresponding properties Task

in the FX application thread. Your UI elements can bind to these properties as needed.

It is recommended that the method call

does not have access to shared data (except for the methods updateXXX(...)

mentioned above). Subclass variables Task

only final

, try the call()

compute value method and return the value.

For cancellation, the Task

class Task

defines a built-in method cancel()

. If you have a long running method call()

, you should check the value periodically isCancelled()

and stop doing work if it returns true

.

Here's a basic example:

import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import javafx.application.Application;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.ProgressBarTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;

public class FileTaskExample extends Application {

    private static final Random RNG = new Random();

    private static final int MAX_THREADS = 4 ;

    private final Executor exec = Executors.newFixedThreadPool(MAX_THREADS, runnable -> {
        Thread t = new Thread(runnable);
        t.setDaemon(true);
        return t ;
    });

    @Override
    public void start(Stage primaryStage) {

        // table to display all tasks:
        TableView<FileProcessingTask> table = new TableView<>();

        TableColumn<FileProcessingTask, File> fileColumn = new TableColumn<>("File");
        fileColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<File>(cellData.getValue().getFile()));
        fileColumn.setCellFactory(col -> new TableCell<FileProcessingTask, File>() {
            @Override
            public void updateItem(File file, boolean empty) {
                super.updateItem(file, empty);
                if (empty) {
                    setText(null);
                } else {
                    setText(file.getName());
                }
            }
        });
        fileColumn.setPrefWidth(200);

        TableColumn<FileProcessingTask, Worker.State> statusColumn = new TableColumn<>("Status");
        statusColumn.setCellValueFactory(cellData -> cellData.getValue().stateProperty());
        statusColumn.setPrefWidth(100);

        TableColumn<FileProcessingTask, Double> progressColumn = new TableColumn<>("Progress");
        progressColumn.setCellValueFactory(cellData -> cellData.getValue().progressProperty().asObject());
        progressColumn.setCellFactory(ProgressBarTableCell.forTableColumn());
        progressColumn.setPrefWidth(100);

        TableColumn<FileProcessingTask, Long> resultColumn = new TableColumn<>("Result");
        resultColumn.setCellValueFactory(cellData -> cellData.getValue().valueProperty());
        resultColumn.setPrefWidth(100);

        TableColumn<FileProcessingTask, FileProcessingTask> cancelColumn = new TableColumn<>("Cancel");
        cancelColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<FileProcessingTask>(cellData.getValue()));
        cancelColumn.setCellFactory(col -> {
            TableCell<FileProcessingTask, FileProcessingTask> cell = new TableCell<>();
            Button cancelButton = new Button("Cancel");
            cancelButton.setOnAction(e -> cell.getItem().cancel());

            // listener for disabling button if task is not running:
            ChangeListener<Boolean> disableListener = (obs, wasRunning, isNowRunning) -> 
                cancelButton.setDisable(! isNowRunning);

            cell.itemProperty().addListener((obs, oldTask, newTask) -> {
                if (oldTask != null) {
                    oldTask.runningProperty().removeListener(disableListener);
                }
                if (newTask == null) {
                    cell.setGraphic(null);
                } else {
                    cell.setGraphic(cancelButton);
                    cancelButton.setDisable(! newTask.isRunning());
                    newTask.runningProperty().addListener(disableListener);
                }
            });

            return cell ;
        });
        cancelColumn.setPrefWidth(100);

        table.getColumns().addAll(Arrays.asList(fileColumn, statusColumn, progressColumn, resultColumn, cancelColumn));

        Button cancelAllButton = new Button("Cancel All");
        cancelAllButton.setOnAction(e -> 
            table.getItems().stream().filter(Task::isRunning).forEach(Task::cancel));

        Button newTasksButton = new Button("Process files");
        FileChooser chooser = new FileChooser();
        newTasksButton.setOnAction(e -> {
            List<File> files = chooser.showOpenMultipleDialog(primaryStage);
            if (files != null) {
                files.stream().map(FileProcessingTask::new).peek(exec::execute).forEach(table.getItems()::add);
            }
        });

        HBox controls = new HBox(5, newTasksButton, cancelAllButton);
        controls.setAlignment(Pos.CENTER);
        controls.setPadding(new Insets(10));

        BorderPane root = new BorderPane(table, null, null, controls, null);

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static class FileProcessingTask extends Task<Long> {

        private final File file ;

        public FileProcessingTask(File file) {
            this.file = file ;
        }

        public File getFile() {
            return file ;
        }

        @Override
        public Long call() throws Exception {

            // just to show you can return the result of the computation:
            long fileLength = file.length();

            // dummy processing, in real life read file and do something with it:
            int delay = RNG.nextInt(50) + 50 ;
            for (int i = 0 ; i < 100; i++) {
                Thread.sleep(delay);
                updateProgress(i, 100);

                // check for cancellation and bail if cancelled:
                if (isCancelled()) {
                    updateProgress(0, 100);
                    break ;
                }
            }

            return fileLength ;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

      

+7


source







All Articles