我有一个 JavaFX 应用程序,其中一项功能涉及搜索(大型)二进制数据以查找特定模式。为了保持 GUI 的响应能力,我创建了一个并发任务并将其放在不同的线程上:
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();
}
这工作得很好,但是将(库)功能与 GUI 代码混合在一起。特别是,我想将搜索代码放入一个单独的库中,为该功能编写测试用例 - 完全自动化,无需 GUI - 当然,当从命令行进行测试时,不需要单独的线程。现在,我只能将
pattern.matches(entries.get(i))
放入该库中。更干净的方法是有一个函数
searchpattern(filename) {
for(int=0;i< ... ;i++) {
...
}
return observableArrayList(...);
}
在包含完整循环的库中,编写我的测试用例和基准测试,然后以某种方式将该函数包含在 GUI 代码中。分离这段代码时我面临的问题是:
updateProgress()
需要对任务的引用。当然,我总是可以在尝试实现测试用例时创建一个任务,即即使从非 GUI 上下文调用 searchpattern(filename)
时也是如此。我可以简单地忽略代码中的任何进度更新,但这会造成非常糟糕的用户体验。
是否有其他方法可以使用某些抽象层从(可能)长时间运行的函数传递更新,这使得该函数在库中从(线程)GUI 上下文和非线程命令行上下文都很容易?
这里有两种方法。为了让事情更具体,假设我们有一个类在列表上实现一个可能长时间运行的进程:
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);
}
(我在这里对列表在处理过程中未被修改等做出了大量假设)
第一种方法是如@Slaw在评论中所描述的。在这种方法中,您的外部库类保留一个侦听器列表,当处理的项目数发生变化时,这些侦听器会收到通知。您可以简单地使用
DoubleConsumer
来实现此目的。例如:
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);
}
您可以在任务中使用它,如下所示:
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);
这依赖于您的库类实现侦听器通知机制。如果您不想这样做,或者您正在使用某些未实现它的第三方库,则可以轮询处理对象以了解其当前进度。我会从
AnimationTimer
进行轮询,尽管其他方法也是可能的。最低要求(据我所知)是处理对象有一种方法以线程安全的方式报告其进度(因为您将从不同的线程轮询到正在更新它的线程) .
所以,例如:
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);
}
然后
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);