Properly doing multithreading and thread pools with JavaFX Tasks

后端 未结 1 653
暗喜
暗喜 2021-01-05 06:58

I have an option for users to submit multiple files from a FileChooser to be processed by some code. The result will be IO for reading the file, then the actual heavy comput

相关标签:
1条回答
  • 2021-01-05 07:15

    JavaFX has a javafx.concurrent API; in particular, the Task class fits your use case very nicely. This API is designed to work in conjunction with the java.util.concurrent API. For example, Task is an implementation of FutureTask, so it can be submitted to an Executor. As you want to use a thread pool, you can create an Executor that implements a thread pool for you, and submit your tasks to it:

    final int MAX_THREADS = 4 ;
    
    Executor exec = Executors.newFixedThreadPool(MAX_THREADS);
    

    As these threads are running in the background of a UI application, you probably don't want them to prevent application exit. You can achieve this by making the 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 no threads are available, they will wait in a queue until a thread becomes available.

    To implement the actual Task, there are a few things to bear in mind:

    You must not update the UI from a background thread. Since your Task is submitted to the executor above, it's call() method will be invoked on a background thread. If you really need to change the UI during the execution of the call method, you can wrap the code that changes the UI in Platform.runLater(...), but it is better to structure things so that you avoid this situation. In particular, the Task has a set of updateXXX(...) methods that change the values of corresponding Task properties on the FX Application thread. Your UI elements can bind to these properties as needed.

    It is advisable for the call method not to access any shared data (other than via the updateXXX(...) methods mentioned above). Instantiate your Task subclass setting only final variables, have the call() method compute a value, and return the value.

    For canceling the Task, the Task class defines a built-in cancel() method. If you have a long-running call() method, you should periodically check the value of 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);
        }
    }
    
    0 讨论(0)
提交回复
热议问题