Waiting for multiple SwingWorkers
Asked Answered
M

2

12

Please consider the following code fragment:

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import javax.swing.*;

public class TestApplet extends JApplet
{
    @Override
    public void init()
    {
        try
        {
            SwingUtilities.invokeAndWait(new Runnable()
            {
                @Override
                public void run()
                {
                    createGUI();
                }
            });
        }
        catch(InterruptedException | InvocationTargetException ex)
        {
        }
    }

    private void createGUI()
    {
        getContentPane().setLayout(new FlowLayout());
        JButton startButton = new JButton("Do work");
        startButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent ae)
            {
                JLabel label = new JLabel();
                new Worker(label).execute();
            }
        });
        getContentPane().add(startButton);
    }

    private class Worker extends SwingWorker<Void, Void>
    {
        JLabel label;

        public Worker(JLabel label)
        {
            this.label = label;
        }

        @Override
        protected Void doInBackground() throws Exception
        {
            // do work
            return null;
        }

        @Override
        protected void done()
        {
            getContentPane().remove(label);
            getContentPane().revalidate();
        }
    }
}

Here is add a label to the applet that displays some intermediate results of the Worker thread (using publish/process methods). At the end, the label is removed from the applet's pane. My question is, how could I create several labels, each with its own Worker thread, and remove them when they are all done?

Thanks in advance.

UPDATE:

I hope this will clarify my question. I'd like the labels to be removed all at once, when all of the workers have finished their tasks, not immediately after each worker has finished.

UPDATE 2:

The following code seems to be doing what I need. Please comment whether I did it the right way. I have a feeling there is something wrong. One problem is that the labels to the right of the button remain visible although they are removed. setVisible(false) seems to solve this issue. Is that the way to do it?

import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.swing.*;

public class TestApplet extends JApplet
{
    private Queue<JLabel> labels = new LinkedList<>();
    private static final Random rand = new Random();

    @Override
    public void init()
    {
        try
        {
            SwingUtilities.invokeAndWait(new Runnable()
            {
                @Override
                public void run()
                {
                    createGUI();
                }
            });
        }
        catch(InterruptedException | InvocationTargetException ex){}
    }

    private void createGUI()
    {
        getContentPane().setLayout(new FlowLayout());
        JButton startButton = new JButton("Do work");
        startButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent ae)
            {
                ExecutorService executor = Executors.newFixedThreadPool(10);
                for(int i = 0; i < 10; i++)
                {
                    JLabel label = new JLabel();
                    getContentPane().add(label);
                    executor.execute(new Counter(label));
                }
            }
        });
        getContentPane().add(startButton);
    }

    private class Counter extends SwingWorker<Void, Integer>
    {
        private JLabel label;

        public Counter(JLabel label)
        {
            this.label = label;
        }

        @Override
        protected Void doInBackground() throws Exception
        {
            for(int i = 1; i <= 100; i++)
            {
                publish(i);
                Thread.sleep(rand.nextInt(80));
            }

            return null;
        }

        @Override
        protected void process(List<Integer> values)
        {
            label.setText(values.get(values.size() - 1).toString());
        }

        @Override
        protected void done()
        {
            labels.add(label);

            if(labels.size() == 10)
            {
                while(!labels.isEmpty())
                    getContentPane().remove(labels.poll());

                getContentPane().revalidate();
            }
        }
    }
}
Massey answered 6/7, 2012 at 16:42 Comment(4)
You might also consider putting them in a list (JList).Archerfish
@AndrewThompson, Yep, but where should I place the code that will wait them? Should it be another SwingWorker spawning several others?Massey
Please don't swallow exceptions.Daugherty
@Daugherty Thanks. Definitely. This is just an example.Massey
D
16

I intend to remove all of the labels together when all of the workers have completed their tasks.

As described here, a CountDownLatch works well in this context. In the example below, each worker invokes latch.countDown() on completion, and a Supervisor worker blocks on latch.await() until all tasks complete. For demonstration purposes, the Supervisor updates the labels. Wholesale removal, shown in comments, is technically possible but generally unappealing. Instead, consider a JList or JTable.

Worker Latch Test

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.swing.*;

/**
* @see https://mcmap.net/q/226325/-waiting-for-multiple-swingworkers
* @see https://mcmap.net/q/226427/-swing-worker-threads-not-concurrent
*/
public class WorkerLatchTest extends JApplet {

    private static final int N = 8;
    private static final Random rand = new Random();
    private Queue<JLabel> labels = new LinkedList<JLabel>();
    private JPanel panel = new JPanel(new GridLayout(0, 1));
    private JButton startButton = new JButton(new StartAction("Do work"));

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.setTitle("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new WorkerLatchTest().createGUI());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    @Override
    public void init() {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                add(new WorkerLatchTest().createGUI());
            }
        });
    }

    private JPanel createGUI() {
        for (int i = 0; i < N; i++) {
            JLabel label = new JLabel("0", JLabel.CENTER);
            label.setOpaque(true);
            panel.add(label);
            labels.add(label);
        }
        panel.add(startButton);
        return panel;
    }

    private class StartAction extends AbstractAction {

        private StartAction(String name) {
            super(name);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
                startButton.setEnabled(false);
                CountDownLatch latch = new CountDownLatch(N);
                ExecutorService executor = Executors.newFixedThreadPool(N);
                for (JLabel label : labels) {
                    label.setBackground(Color.white);
                    executor.execute(new Counter(label, latch));
                }
                new Supervisor(latch).execute();
        }
    }

    private class Supervisor extends SwingWorker<Void, Void> {

        CountDownLatch latch;

        public Supervisor(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        protected Void doInBackground() throws Exception {
            latch.await();
            return null;
        }

        @Override
        protected void done() {
            for (JLabel label : labels) {
                label.setText("Fin!");
                label.setBackground(Color.lightGray);
            }
            startButton.setEnabled(true);
            //panel.removeAll(); panel.revalidate(); panel.repaint();
        }
    }

    private static class Counter extends SwingWorker<Void, Integer> {

        private JLabel label;
        CountDownLatch latch;

        public Counter(JLabel label, CountDownLatch latch) {
            this.label = label;
            this.latch = latch;
        }

        @Override
        protected Void doInBackground() throws Exception {
            int latency = rand.nextInt(42) + 10;
            for (int i = 1; i <= 100; i++) {
                publish(i);
                Thread.sleep(latency);
            }
            return null;
        }

        @Override
        protected void process(List<Integer> values) {
            label.setText(values.get(values.size() - 1).toString());
        }

        @Override
        protected void done() {
            label.setBackground(Color.green);
            latch.countDown();
        }
    }
}
Daugherty answered 7/7, 2012 at 6:50 Comment(6)
You're welcome; a hybrid applet/application like this offers a variety of deployment options using java-web-start, as shown here.Daugherty
+1 excellent answer and code example! (nitpicking unrelated to the core of the question: usage of ActionListener, cough :-)Woodruff
@kleopatra: Thank you for the reminder; Action is more flexible.Daugherty
Could you please elaborate on the advantage of Action instead of ActionListener in this case?Massey
Yes, but not as well as the relevant tutorial.Daugherty
Your code helped me a lot, thank you :) . (PS: The +1 is implied...)Gunslinger
E
1

The code that you have is already doing that to a certain extent. You need to actually add the label to the contentpane when the button is clicked. Something like this:

 JLabel label = new JLabel();
 getContentPane().add(label);
 getContentPane().validate();
 new Worker(label).execute();

It may be a good idea to put some text in the label so you actually see it when it is added to the screen.

 JLabel label = new JLabel("Hello...I am here");

And finally in the doInBackground() method you can add some code to update the label as some task is running:

 for(int i = 0;i < 100; i++){
            Thread.sleep(20);
            label.setText("Counting..." + i);
  }

This way you actually see the task running. If you click the button multiple times you see multiple labels and they each disappear after the task is completed.

Eloquence answered 6/7, 2012 at 17:7 Comment(1)
Thanks for fast reply. I simplified the code before pasting it here. The label indeed has some text and is added to the content pane. :) Thanks for suggestions though. I think I didn't state my question clearly. What I intend is to remove all of the labels together when all of their workers have completed the tasksMassey

© 2022 - 2024 — McMap. All rights reserved.