Ответ 1
КРАТКОЙ ОТВЕТ:
Это происходит из-за того, что publish() напрямую не платит process
, он устанавливает таймер, который будет запускать планирование блока process() в EDT после DELAY
. Поэтому, когда работник отменен, все еще есть таймер, ожидающий запланировать процесс() с данными последнего опубликования. Причина использования таймера заключается в реализации оптимизации, когда один процесс может выполняться с объединенными данными нескольких изданий.
ДОЛГОЙ ОТВЕТ:
Посмотрим, как publish() и cancel взаимодействуют друг с другом, для чего давайте погрузиться в какой-то исходный код.
Сначала легкая часть, cancel(true)
:
public final boolean cancel(boolean mayInterruptIfRunning) {
return future.cancel(mayInterruptIfRunning);
}
Это отмена завершает вызов следующего кода:
boolean innerCancel(boolean mayInterruptIfRunning) {
for (;;) {
int s = getState();
if (ranOrCancelled(s))
return false;
if (compareAndSetState(s, CANCELLED)) // <-----
break;
}
if (mayInterruptIfRunning) {
Thread r = runner;
if (r != null)
r.interrupt(); // <-----
}
releaseShared(0);
done(); // <-----
return true;
}
Состояние SwingWorker установлено на CANCELLED
, поток прерывается и вызывается done()
, но это не выполняется SwingWorker, а future
done(), который указан, когда переменная создается в Конструктор SwingWorker:
future = new FutureTask<T>(callable) {
@Override
protected void done() {
doneEDT(); // <-----
setState(StateValue.DONE);
}
};
И код doneEDT()
:
private void doneEDT() {
Runnable doDone =
new Runnable() {
public void run() {
done(); // <-----
}
};
if (SwingUtilities.isEventDispatchThread()) {
doDone.run(); // <-----
} else {
doSubmit.add(doDone);
}
}
Что вызывает SwingWorkers done()
напрямую, если мы находимся в EDT, который является нашим делом. В этот момент SwingWorker должен остановиться, не более publish()
следует вызвать, это достаточно просто продемонстрировать со следующей модификацией:
while(!isCancelled()) {
textArea.append("Calling publish\n");
publish("Writing...\n");
}
Однако мы все еще получаем сообщение "Написание..." от process(). Итак, давайте посмотрим, как называется процесс(). Исходным кодом для publish(...)
является
protected final void publish(V... chunks) {
synchronized (this) {
if (doProcess == null) {
doProcess = new AccumulativeRunnable<V>() {
@Override
public void run(List<V> args) {
process(args); // <-----
}
@Override
protected void submit() {
doSubmit.add(this); // <-----
}
};
}
}
doProcess.add(chunks); // <-----
}
Мы видим, что run()
Runnable doProcess
- это тот, кто заканчивает вызов process(args)
, но этот код просто вызывает doProcess.add(chunks)
not doProcess.run()
и там тоже doSubmit
. Посмотрим doProcess.add(chunks)
.
public final synchronized void add(T... args) {
boolean isSubmitted = true;
if (arguments == null) {
isSubmitted = false;
arguments = new ArrayList<T>();
}
Collections.addAll(arguments, args); // <-----
if (!isSubmitted) { //This is what will make that for multiple publishes only one process is executed
submit(); // <-----
}
}
Итак, что действительно делает publish()
, это добавление кусков в некоторый внутренний ArrayList arguments
и вызов submit()
. Мы только что увидели, что представляют только вызовы doSubmit.add(this)
, который является тем же самым методом add
, поскольку оба doProcess
и doSubmit
расширяют AccumulativeRunnable<V>
, однако на этот раз V
находится Runnable
вместо String
> как в doProcess
. Таким образом, кусок - это runnable, который вызывает process(args)
. Однако вызов submit()
- это совершенно другой метод, определенный в классе doSubmit
:
private static class DoSubmitAccumulativeRunnable
extends AccumulativeRunnable<Runnable> implements ActionListener {
private final static int DELAY = (int) (1000 / 30);
@Override
protected void run(List<Runnable> args) {
for (Runnable runnable : args) {
runnable.run();
}
}
@Override
protected void submit() {
Timer timer = new Timer(DELAY, this); // <-----
timer.setRepeats(false);
timer.start();
}
public void actionPerformed(ActionEvent event) {
run(); // <-----
}
}
Создает таймер, который запускает код actionPerformed
один раз после DELAY
miliseconds. После запуска события код будет помечен в EDT, который вызовет внутренний run()
, который заканчивается вызовом run(flush())
из doProcess
и, таким образом, выполняется process(chunk)
, где chunk - это сброшенные данные arguments
ArrayList. Я пропустил некоторые детали, цепочка вызовов "run" выглядит так:
- doSubmit.run()
- doSubmit.run(flush())//На самом деле цикл runnables, но будет иметь только один (*)
- doProcess.run()
- doProcess.run(flush())
- процесс (фрагмент)
(*) Логические isSubmited
и flush()
(которые сбрасывают это логическое значение) делают так, что дополнительные вызовы для публикации не добавляют doProcess runnables для вызова в doSubmit.run(flush()), однако их данные не игнорируется. Таким образом, выполнение одного процесса для любого количества изданий, вызванных в течение срока действия таймера.
В целом, то, что publish("Writing...")
выполняет, выполняет планирование вызова process(chunk)
в EDT после DELAY. Это объясняет, почему даже после того, как мы отменили поток и больше не публикуется, все еще выполняется одно выполнение процесса, поскольку в тот момент, когда мы отменяем рабочего там (с большой вероятностью), таймер, который запланирует process()
после done()
уже планируется.
Почему этот таймер используется вместо планирования процесса() в EDT с помощью invokeLater(doProcess)
? Чтобы реализовать оптимизацию производительности, описанную в docs:
Поскольку метод процесса вызывается асинхронно на Событии Dispatch Thread может вызвать несколько вызовов для метода публикации перед выполнением метода процесса. Для достижения всех целей эти призывы объединяются в один вызов с помощью конкатенированных аргументы. Например:
publish("1"); publish("2", "3"); publish("4", "5", "6"); might result in: process("1", "2", "3", "4", "5", "6")
Теперь мы знаем, что это работает, потому что все публикации, которые происходят в интервале DELAY, добавляют их args
в эту внутреннюю переменную, которую мы видели arguments
, и process(chunk)
будет выполнять все эти данные за один раз.
ЭТО БЫСТРО? Временное решение?
Трудно сказать, если это ошибка или нет. Может быть, имеет смысл обрабатывать данные, которые опубликовал фоновый поток, поскольку работа действительно выполнена, и вам может быть интересно получить обновление графического интерфейса с такой же информацией как вы можете (если это то, что делает process()
, например). И тогда, возможно, не имеет смысла, если done()
требует, чтобы все обработанные данные и/или вызов process() после того, как done() создал несоответствия данных /GUI.
Существует очевидное обходное решение, если вы не хотите, чтобы какой-либо новый процесс() выполнялся после выполнения(), просто проверьте, отменен ли рабочий в методе process
!
@Override
protected void process(List<String> chunks) {
if (isCancelled()) return;
String string = chunks.get(chunks.size() - 1);
textArea.append(string);
}
Сложнее сделать make() выполнимым после этого последнего процесса(), например, сделать может просто использовать также таймер, который запланирует фактическую работу done() после > DELAY. Хотя я не могу думать, что это было бы обычным делом, поскольку, если вы отменили, не должно быть важно пропустить еще один процесс(), когда мы знаем, что мы фактически отменяем выполнение всех будущих.