1

I want to keep my user informed of the progress of an I/O operation. At the moment I've got an inner class that I kick off before I start my I/O and stop after it's done. It looks like this:

class ProgressUpdater implements Runnable {

        private Thread thread;
        private long last = 0;
        private boolean update = true;
        private long size;

        public ProgressUpdater(long size) {
            this.size = size;
            thread = new Thread(this);
        }

        @Override
        public void run() {
            while (update) {
                if (position > last) {
                    last = position;
                    double progress = (double) position / (double) size * 100d;
                    parent.setProgress((int) progress);
                }
            }
        }

        public void start() {
            thread.start();
        }

        public void stop() {
            update = false;
            parent.setProgress(100);
        }
    }

parent is my reference to my UI and position is a field in my outer class that represents how far in the I/O we have progressed. I set progress to 100% when stopping because sometimes the I/O finishes and stops my updater before it can finish updating the previous increment. That just ensures it's at 100%.

At the moment, this works, and I use it like this:

ProgressUpdater updater = new ProgressUpdater(file.length());
updater.start();
//do I/O
//...
updater.stop();

The problem is that the loop eats CPU pretty badly. I tried throwing a lock (with a wait/notify) in there but I don't know what I'm doing when it comes to using wait/notify so it just hung my thread. What can I do to stop it from using so many CPU cycles?

mKorbel
  • 109,525
  • 20
  • 134
  • 319
ldam
  • 4,412
  • 6
  • 45
  • 76
  • Something seems wrong to me. Its looping even if there is no progress done. Theres no sort of wait or condition to loop again. It will loop aiminglessly testing `if (position > last)` a million times even if all of the times it fails. There should be some sort of `sleep()` in this thing. – Havenard Mar 29 '13 at 20:28
  • 1
    You don't have to update stuff right away, you can update every 100ms and to the user it will be no different to a real time update. – Havenard Mar 29 '13 at 20:29
  • @Havenard I ended going with your suggestion, I just added a sleep in there and now it works like a dream. If you want to answer this old question I forgot about, I'll accept it. – ldam Jun 09 '13 at 17:09

2 Answers2

6

You should look into using a SwingWorker. Basically you do all the IO on the background thread that the SwingWorker provides, and there are built-in ways of reporting the progress to the swing thread.

Then whenever you update that position, you can automatically update the progress, rather than polling continuously, which is what you've done so far.

Take a look at Swingworker Timeout or SwingWorker with FileReader to see if either will help.

An alternative solution, if you don't want to use a SwingWorker, would probably just be to, instead of updating that position value, update the progress bar directly, with a call like this:

SwingUtilities.invokeLater(new Runnable() {
    @Override public void run() {
        getMyProgressBar().setValue(position);
    }
});

Assuming you've set up the progress bar so that its max is the size of the file.

Community
  • 1
  • 1
Rob I
  • 5,627
  • 2
  • 21
  • 28
  • I would say `SwingWorker` is the "normal" way to do what you've done, if the application is primarily a Swing app. You can definitely do it with whatever other background app you like. I'll make a little edit here with another way. – Rob I Mar 29 '13 at 20:27
  • 1
    @LoganDam No, but it would easier to re-sync the calls back to the UI as you should NEVER, EVER create or modify any UI component from any thread other then the EDT. Converting the IO to use the `SwingWorker` is really trivial. Check out [this](http://stackoverflow.com/questions/15668715/how-to-use-the-swing-timer-to-delay-the-loading-of-a-progress-bar/15669717#15669717) example for a detailed example – MadProgrammer Mar 29 '13 at 20:30
  • Just btw, when I call `parent.setProgress()` that uses `java.awt.EventQueue.invokeLater()` so everything is on my UI thread already :) – ldam Mar 29 '13 at 20:32
  • Oh and I made this little thread here to keep my IO as fast as possible. I've tested, if I update my progress bar directly from my IO as your edit suggests, I go from 60mb/sec to roughly 30mb/sec. Not optional for me. – ldam Mar 29 '13 at 20:35
  • Hmm. Then I would "get smart" about some other thread coordination. Off the top of my head, the code you set the position in could also release a [Semaphore](http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/Semaphore.html) (perhaps only after some threshold of progress), and the while loop above would wait, aquiring that semaphore. Wait/notify would work as well. – Rob I Mar 29 '13 at 20:40
  • 2
    @LoganDam Without seeing the code you are using (for the SwingWorker), it's difficult to point out areas of oppurtunity. But if you use publish, this tends to consolidate the process calls into a list, which should help – MadProgrammer Mar 29 '13 at 20:48
5

First of all never use Thread to update GUI (Swing components) in java. Instead use javax.swing.Timer or javax.swing.SwingWorker. And for Basic I/O operation use ProgressMonitorInputStream .
As simple example of reading a file and displaying it in JTextArea and with progrressbar..Have a look at the code given below:

enter image description here

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.io.*;
import java.beans.*;
class ProgressBarFrame extends JFrame 
{
    JProgressBar progressBar;
    int BUFFERSIZE = 10;
    JTextArea textArea;
    MyWorker worker;
    private void createAndShowGUI()
    {
        progressBar = new JProgressBar(0,100);
        progressBar.setStringPainted(true);
        JButton button = new JButton("Read File");
        textArea = new JTextArea(30,100);
        JScrollPane jsp = new JScrollPane(textArea);
        Container c = getContentPane();
        c.add(jsp);
        c.add(progressBar,BorderLayout.NORTH);
        c.add(button,BorderLayout.SOUTH);
        worker = new MyWorker();
        button.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent evt)
            {
                textArea.setText("");
                progressBar.setValue(0);
                if (worker.getState()== SwingWorker.StateValue.DONE || worker.getState()==SwingWorker.StateValue.STARTED)
                {
                    worker.cancel(true);
                    worker = new MyWorker();
                }
                worker.execute();

            }
        });
        pack();
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }
    class MyWorker extends SwingWorker<Void, String> 
    {
        MyWorker()
        {
            addPropertyChangeListener(new PropertyChangeListener() 
            {
                public  void propertyChange(PropertyChangeEvent evt) 
                {
                    if ("progress".equals(evt.getPropertyName())) 
                    {
                        progressBar.setValue((Integer)evt.getNewValue());
                    }
                }
            });
        }

        @Override
        public Void doInBackground() throws IOException
        {
            File file = new File("ProgressBarFrame.java");
            long size = file.length();
            long temp = 0;
            BufferedInputStream bfin = new BufferedInputStream(new FileInputStream(file));
            byte[] buffer=new byte[BUFFERSIZE];
            int totalRead = -1;
            while ((totalRead=bfin.read(buffer))!=-1 && ! isCancelled()) 
            {
                temp = temp + totalRead;
                publish(new String(buffer));
                if (bfin.available()<BUFFERSIZE && bfin.available()!= 0)
                buffer = new byte[bfin.available()];
                else if(bfin.available()==0)
                buffer = new byte[1];
                else
                buffer=new byte[BUFFERSIZE];
                setProgress((int)((temp/(float)size) * 100));
                try{Thread.sleep(1);}catch(Exception ex){}
            }
            setProgress(100);
            return null;
        }
        @Override
        protected void process(List<String> chunks) 
        {
            for (String value : chunks) 
            {
                textArea.append(value);
            }
        }
    }
    public static void main(String st[])
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                ProgressBarFrame pf = new ProgressBarFrame();
                pf.createAndShowGUI();
            }
        });
    }
}
Vishal K
  • 12,976
  • 2
  • 27
  • 38