Как убедиться, что метод done() двух параллельных swingworkers выполняется в EDT в правильном порядке?

#java #swing #concurrency #swingworker

#java #swing #параллелизм #swingworker

Вопрос:

Привет, я пытался узнать, как использовать SwingWorker для обновления графического интерфейса приложений swing.

Мой вопрос заключается в следующем: если у меня есть 2 swingworkers, которые могут выполняться одновременно (каждый запускается с другой кнопки), как убедиться, что их метод done вызывается в EDT (для обновления графического интерфейса пользователя) в том же порядке, в котором они были запущены?

Мои наблюдения и идеи: я попытался синхронизировать их методы doInBackground() с блокировкой, чтобы убедиться, что один должен дождаться, пока другой завершит выполнение своей задачи, прежде чем продолжить. Проблема в том, что я смог (всего несколько раз) увидеть, как swingworker_1 завершает doInBackground() (пока 2-й рабочий ожидает в синхронизированном блоке), но метод done swingworker_2 смог быть вызван EDT до метода done swingworker_1, хотя swingworker_2 закончил после. Я думал об использовании publish непосредственно перед возвратом doInBackground(), это, кажется, всегда работает, но может быть плохой практикой, поскольку, похоже, он используется для промежуточных результатов, а не для конечных результатов? Другой возможностью было бы использовать SwingUtilities.invokeLater внутри doInBackground() для обновления графического интерфейса перед возвратом, опять же, я боюсь, что это может быть и плохой практикой. Заранее спасибо.

Тест, который я использовал, нажав первую кнопку, а затем сразу вторую:

     //Code that runs in the first button
    SwingWorker<Void, Void> swingworker = new SwingWorker<Void, Void>() {

        @Override
        protected Void doInBackground() throws Exception {
            synchronized (lock) {
                System.out.println("Thread 1");

                Thread.sleep(1000);
                return null;
            }

        }

        @Override
        protected void done() {

            System.out.println("Done with thread 1 ");
            //update GUI    
        }

    };
    swingworker.execute();



    //Code that runs in the second button
    SwingWorker<Void, Void> swingworker = new SwingWorker<Void, Void>() {

        @Override
        protected Void doInBackground() throws Exception {
            synchronized (lock) {
                System.out.println("Thread 2");

                return null;
            }

        }

        @Override
        protected void done() {
            System.out.println("Done with thread 2 ");
            //update GUI
        }

    };
    swingworker.execute();
  

ОБНОВЛЕНИЕ: просто чтобы уточнить, меня не интересует, кто запускается первым (swingworker_1 или swingworker_2), это зависит только от того, какая кнопка будет нажата первой. Что я хочу гарантировать, так это то, что если рабочий из кнопки был первым для выполнения (и, следовательно, завершается, поскольку рабочие синхронизируются в методе doInBackground()), то он также должен быть первым в очереди для обновления графического интерфейса пользователя в EDT. Я обнаружил, что это не обязательно происходит, даже если рабочие синхронизированы иногда рабочему, который запускает задачу позже, все равно удается обновить графический интерфейс перед первым.

Комментарии:

1. Интересно, описываете ли вы проблему XY и просили бы вас рассказать подробнее: во-первых, почему этот порядок так важен? Что делают эти рабочие и что делает ваша программа?

2. Сам я стараюсь избегать большого использования done() метода и вместо этого обычно прослушиваю изменения состояния рабочих, используя PropertyChangeListeners. Это освобождает SwingWorker от необходимости выполнять какие-либо вызовы, специфичные для GUI, или даже иметь знания о GUI или любом вызывающем коде.

3. Я не вижу в вашем коде ничего, что гарантировало бы, что первый рабочий получит lock монитор раньше, чем второй.

4. Привет, Hovercraft, идея заключалась в том, чтобы использовать первый swingworker / button для запуска фоновых данных, а по завершении он обновит метку или что-то в «Онлайн». Вторая кнопка может использоваться для завершения работы, и она будет отвечать за завершение всех фоновых потоков, после завершения она обновит метку до Offline.

5. Привет, Джон, идея состоит не в том, чтобы убедиться, что один из них запускается раньше другого, а в том, что при запуске одного swingworker он обновляет графический интерфейс перед другим, поэтому, если вторая кнопка нажата раньше, она должна обновить графический интерфейс перед кнопкой 1 и наоборот.

Ответ №1:

Если вы согласны с тем, что рабочие выполняются последовательно, что и произойдет, если вы синхронизируете блокировку в соответствии с вашим образцом кода, и вы не используете какие-либо другие функции SwingWorker , кроме doInBackground() and done() , вам может быть проще использовать однопоточный исполнитель и SwingUtilities.invokeLater(...) представить результаты в графическом интерфейсе.

Таким образом, вам гарантируется, что вещи, отправленные исполнителю, будут выполняться в том порядке, в котором они были отправлены.

Если это работает для вас, вы можете попробовать что-то вроде этого:

 final ExecutorService executor = Executors.newSingleThreadExecutor();

// Code that runs in the first button
Runnable worker1 = new Runnable() {
  @Override
  public void run() {
    System.out.println("Thread 1");
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      e.printStackTrace(System.err);
    }
    SwingUtilities.invokeLater(this::updateGUI);
  }

  private void updateGUI() {
    System.out.println("Done with thread 1");
    //update GUI
  }
};
executor.execute(worker1);

// Code that runs in the second button
Runnable worker2 = new Runnable() {
  @Override
  public void run() {
    System.out.println("Thread 2");
    SwingUtilities.invokeLater(this::updateGUI);
  }

  private void updateGUI() {
    System.out.println("Done with thread 2");
    //update GUI
  }
};
executor.execute(worker2);
  

Комментарии:

1. Привет, msandiford, это, безусловно, будет работать так, как вы сказали, что рабочие выполняются один за другим, и они ставят в очередь обновление графического интерфейса перед возвратом. Чтобы адаптировать это, я бы выполнил worker1 в button1 и worker2 в button2. Как я уже говорил в своем вопросе изначально, я убежден, что использование invokelater внутри doInBackground() swingworkers будет работать так же хорошо, и я думаю, что publish также поможет, просто их можно считать плохой практикой, каждая по другой причине? Просто кажется странным, что done не обязательно будет стоять в очереди в том же порядке, в котором завершаются рабочие.

2. Вы могли бы использовать synchronized блок в doInBackground и вызывать invokeLater внутри синхронизированного блока, но в конечном SwingWorker итоге использует многопоточный исполнитель, поэтому нет реальной гарантии относительно того, какой рабочий будет запущен первым для SwingWorkers, отправленных почти одновременно. Учитывая, что отправка рабочих данных основана на нажатии кнопок GUI, это может не иметь большого значения для вашего варианта использования.

3. Точно, msandiford, в любом случае не имеет значения, кто запускается первым, просто тот, который запускается первым, сначала поставит в очередь на обновление графического интерфейса. Кажется, что invokelater, будь то ваш подход (который, кстати, хорош и умен) или внутри синхронизированного с swingworker doInBackground() , обеспечит выполнение этого порядка. Метод done, с другой стороны, не может этого гарантировать. Предложенный ранее PropertyChangeListener также может быть элегантным решением, но я боюсь, что он также может не гарантировать правильный порядок, поскольку может возникнуть задержка между завершением doInBackground() и изменением статуса done.