Запуск потоков из кода обработчика событий EDT в приложениях Swing - PullRequest
1 голос
/ 25 сентября 2019

Насколько я понимаю, поток диспетчера событий Swing (EDT) состоит в том, что он является специальным потоком, в котором выполняется код обработки событий.Итак, если мое понимание верно, то в приведенном ниже примере:

private class ButtonClickListener implements ActionListener{
   public void actionPerformed(ActionEvent e) {
      // START EDT
      String command = e.getActionCommand();  

      if( command.equals( "OK" ))  {
         statusLabel.setText("Ok Button clicked.");
      } else if( command.equals( "Submit" ) )  {
         statusLabel.setText("Submit Button clicked.");
      } else {
         statusLabel.setText("Cancel Button clicked.");
      }     
      // END EDT
   }        
}

Весь код между START EDT и END EDT выполняется в EDT, и любой код вне его выполняется восновная ветка приложения.Точно так же, другой пример:

// OUTSIDE EDT
JFrame mainFrame = new JFrame("Java SWING Examples");
mainFrame.setSize(400,400);
mainFrame.setLayout(new GridLayout(3, 1));
mainFrame.addWindowListener(new WindowAdapter() {
   public void windowClosing(WindowEvent windowEvent){
      // START EDT
      System.exit(0);
      // END EDT
   }        
   // BACK TO BEING OUTSIDE THE EDT
});  

Опять же, только System.exit(0) выполняется внутри EDT.

Так что для начала, если мое понимание "разделения труда"между EDT и кодом основного потока приложения выполняется неправильно, пожалуйста, начните исправлять меня!

Теперь я натолкнулся на статью, в которой подчеркивалось использование создания нового Thread изнутри всего этого EDT.код, из-за которого мой первый пример, приведенный выше, выглядел бы так:

public class LabelUpdater implements Runnable {
  private JLabel statusLabel;
  private ActionEvent actionEvent;

  // ctor omitted here for brevity

  @Override
  public void run() {
    String command = actionEvent.getActionCommand();  

    if (command.equals( "OK" ))  {
       statusLabel.setText("Ok Button clicked.");
    } else if( command.equals( "Submit" ) )  {
       statusLabel.setText("Submit Button clicked.");
    } else {
       statusLabel.setText("Cancel Button clicked.");
    }   
  }
}

private class ButtonClickListener implements ActionListener{
   public void actionPerformed(ActionEvent e) {
      // START EDT
      Thread thread = new Thread(new LabelUpdater(statusLabel, e));
      thread.start();
      // END EDT
   }        
}

Мой вопрос: какое преимущество (или его отсутствие) в этом подходе?Должен ли я всегда кодировать свой код EDT таким образом, или есть рубрика, которой необходимо следовать в качестве руководства для , когда применять его?Заранее спасибо!

1 Ответ

5 голосов
/ 25 сентября 2019

Вопрос немного широкий и неопределенный, но я постараюсь ответить на некоторые из вопросов, которые вы задали.Отправной точкой для дальнейших собственных исследований, вероятно, является Урок: параллелизм в Swing , хотя из этого действительно может быть сложно получить определенные утверждения для конкретных случаев.

Прежде всего, естьявляется всеобъемлющим правилом в Swing, которое называется Правило однопотоковой обработки :

После реализации компонента Swing весь код, который может влиять или зависеть от состояния этогокомпонент должен выполняться в потоке диспетчеризации событий.

(К сожалению, в учебнике об этом больше не говорится так четко)


Помня об этом, глядя на ваши фрагменты:

// OUTSIDE EDT
JFrame mainFrame = new JFrame("Java SWING Examples");
...

Это часто так, к сожалению - и, к сожалению, даже в некоторых официальных примерах Swing.Но это уже может вызвать проблемы.Чтобы быть в безопасности, GUI (включая основную раму) всегда должен обрабатываться на EDT, используя SwingUtilities#invokeLater.Тогда шаблон всегда один и тот же:

public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> createAndShowGui());
}

private static void createAndShowGui() {
    JFrame mainFrame = new JFrame("Java SWING Examples");
    ...
    mainFrame.setVisible(true);
}

Что касается второго примера, который вы показали, с классом LabelUpdater: мне было бы любопытно, из какой статьи вы это получили.Я знаю, что существует много cr4p, но этот пример даже отдаленно не имеет смысла ...

public class LabelUpdater implements Runnable {
    private JLabel statusLabel;
    ...

    @Override
    public void run() {
        ...
        statusLabel.setText("Ok Button clicked.");
    }
}

Если этот код (то есть метод run) выполняется в новом потоке, то это явно нарушает правило single thread : статус JLabel изменяется из потока, который является не потоком отправки события!


Основным моментом запуска нового потока в обработчике событий (например, в методе actionPerformed для ActionListener) является предотвращение блокировки пользовательского интерфейса .Если бы у вас был какой-то код, подобный этому

someButton.addActionListener(e -> {
    doSomeComputationThatTakesFiveMinutes();
    someLabel.setText("Finished");
});

, то нажатие кнопки привело бы к блокировке EDT на 5 минут - т.е. графический интерфейс пользователя "завис бы" и выглядел бы так, как будто он завис.В этих случаях (т. Е. Когда у вас длительные вычисления) вы должны выполнять работу в собственном потоке.

Наивный подход к выполнению этого вручную может (примерно) выглядеть следующим образом:

someButton.addActionListener(e -> {
    startBackgroundThread();
});

private void startBackgroundThread() {
    Thread thread = new Thread(() -> {
        doSomeComputationThatTakesFiveMinutes();
        someLabel.setText("Finished");              // WARNING - see notes below!
    });
    thread.start();
}

Теперь нажатие кнопки запускает новый поток, и графический интерфейс больше не блокируется.Но обратите внимание на WARNING в коде: теперь снова возникает проблема изменения JLabel потоком, который является не потоком отправки события!Так что вам придется передать это обратно в EDT:

private void startBackgroundThread() {
    Thread thread = new Thread(() -> {
        doSomeComputationThatTakesFiveMinutes();

        // Do this on the EDT again...
        SwingUtilities.invokeLater(() -> {
            someLabel.setText("Finished");
        });
    });
    thread.start();
}

Это может выглядеть неуклюже и сложно, и, как если бы вам было трудно выяснить, какой поток вы в настоящее время находитесь.И это правильно.Но для обычной задачи запуска длительной задачи есть класс SwingWorker, объясненный в учебном пособии , который делает этот шаблон несколько проще.


Бесстыдная самореклама: Некоторое время назад я создал библиотеку SwingTasks , которая по сути является "Swing Worker на стероидах".Он позволяет вам «связывать» методы, подобные этим ...

SwingTaskExecutors.create(
    () -> computeTheResult(),
    result -> receiveTheResult(result)
).build().execute();

, и обеспечивает отображение (модального) диалога, если выполнение занимает слишком много времени, и предлагает некоторые другие удобные методы, например, для отображенияиндикатор выполнения в диалоге и так далее.Образцы суммированы в https://github.com/javagl/SwingTasks/tree/master/src/test/java/de/javagl/swing/tasks/samples

...