Thorvas
Thorvas

Reputation: 83

Interrupting thread with GUI button in any place of code

Lately I was working on my code which automated many tasks, for example finding path to certain target (by A* algorithm) and moving accordingly through all found nodes in 2D map. I was trying to implement GUI in Swing so I could control the behavior of my code. I created simple snippet:

@Component
public class BotGUI {


    private boolean isRunning = true;

    @Autowired
    private BotInit botInit;

    private Thread botThread;

    public void stopBot() {
        if (this.botThread != null) {
            botThread.interrupt();
        }
    }

    public boolean isRunning() {
        return this.isRunning;
    }

    public BotGUI() {

    }

    public void startGUI() {

        this.botThread = new Thread(botInit);
        botThread.start();

        JFrame jFrame = new JFrame("MagBot Control Panel");
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jFrame.setSize(300, 100);

        JButton toggleButton = new JButton("Stop Bot");
        toggleButton.setBackground(Color.lightGray);
        toggleButton.setForeground(Color.blue);
        toggleButton.setFont(new Font("Arial", Font.BOLD, 16));
        Border lineBorder = BorderFactory.createLineBorder(Color.darkGray);
        toggleButton.setBorder(lineBorder);
        toggleButton.setMargin(new Insets(5, 15, 5, 15));
        toggleButton.setOpaque(true);
        toggleButton.setContentAreaFilled(false);
        toggleButton.addActionListener(e -> {
            if (this.isRunning()) {
                this.stopBot();
                toggleButton.setText("Start Bot");
            } else {
                this.botThread = new Thread(botInit);
                botThread.start();
                toggleButton.setText("Stop Bot");
            }

            this.isRunning = !isRunning;
        });

        jFrame.getContentPane().add(toggleButton, BorderLayout.CENTER);
        jFrame.setVisible(true);
    }
}

This snippet of code tries to interrupt a thread which is running some task. But then a problem appears - to handle interruption correctly, I would have to implement constant isInterrupted() checks or rely on catching InterruptedException and propagating it higher and higher. I have many levels of abstraction in my code and I have problem with handling such a task - I've tried spamming button to stop/start my program and despite having interruption checks in key places in code, it doesn't help. I could have interrupted thread within some for loop which isn't aware of thread interruption. How to handle such problem?

How can I interrupt thread without putting try catch blocks everywhere in code? It doesn't look clean and I doubt it is a good practice.

Example of my code:

            while (!Thread.currentThread().isInterrupted()) {
                bot.processWalkAndAttack();
                TimeUnit.MILLISECONDS.sleep(2000);
            }

        } catch (InterruptedException var3) {

            log.error("Bot stopped.");
        }
    public void processWalkAndAttack() {

        try {
            List<Monster> monsters = utilityFunction.convertToMonsters();
            Monster closestMonster = utilityFunction.findClosestMonster(monsters);
            moveModule.moveToTarget(closestMonster.getPosition());
            utilityFunction.attackMob();
        } catch (InterruptedException e) {
            log.error("Interrupted!");
            Thread.currentThread().interrupt();
        }

    }

I don't have interruption checks in every function call because my code is huge and such checks don't seem to be efficient. So there is a chance that some thread won't be stopped and code will run despite being interrupted because in some for loop or some utility method, we didn't declare scenario when thread is interrupted.

Upvotes: 0

Views: 90

Answers (2)

Hovercraft Full Of Eels
Hovercraft Full Of Eels

Reputation: 285430

Since you're running an animation within a Swing GUI, one way to do a pausable repeated action with a delay is to use a Swing Timer. This would be used in place of the while loop with a 2000 mSec sleep. Then, if you need to pause the animation, simply call stop() on the timer, and to restart the timer, simply call restart()

For example, in place of

try {
    while (!Thread.currentThread().isInterrupted()) {
        bot.processWalkAndAttack();
        TimeUnit.MILLISECONDS.sleep(2000);
    }
} catch (InterruptedException var3) {
    log.error("Bot stopped.");
}

you could do

Timer timer = new Timer(2000, e -> {
    bot.processWalkAndAttack();
});

To start the timer you'd call timer.start(); and to stop it, say if a button is pressed, call timer.stop();

There would be no need for direct use of threads and no need for interrupts or try/catch.

For example:

import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.KeyEvent;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class SwingFoo01 extends JPanel {
    private static final int PREF_W = 800;
    private static final int PREF_H = 650;
    private static final String START = "Start";
    private static final String STOP = "Stop";
    private static final int DELAY = 20;
    private static final int OVAL_W = 20;
    private static final int DELTA = 3;
    private JButton startStopButton = new JButton(START);
    private Timer timer = new Timer(DELAY, new TimerListener());
    private int xPos = 0;
    private int yPos = 0;
    

    public SwingFoo01() {
        startStopButton.setMnemonic(KeyEvent.VK_S);
        startStopButton.addActionListener(new ButtonListener());

        add(startStopButton);
        setPreferredSize(new Dimension(PREF_W, PREF_H));
        
    }

    // paintComponent, draw circle at xPos, yPos. Timer will move it semi-randomly from upper left to lower right
    // and back again.
    @Override
    protected void paintComponent(java.awt.Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setColor(java.awt.Color.RED);
        g.fillOval(xPos, yPos, OVAL_W, OVAL_W);
    }


    private class ButtonListener implements java.awt.event.ActionListener {
        public void actionPerformed(java.awt.event.ActionEvent e) {
            if (startStopButton.getText().equals(START)) {
                startStopButton.setText(STOP);
                timer.start();
            } else {
                startStopButton.setText(START);
                timer.stop();
            }
        }
    }

    // animate semi-random motion of the circle
    private class TimerListener implements java.awt.event.ActionListener {
        public void actionPerformed(java.awt.event.ActionEvent e) {
            xPos += (int) (Math.random() * DELTA + 1);
            yPos += (int) (Math.random() * DELTA + 1);
            if (xPos + OVAL_W > getWidth()) {
                xPos = 0;
            }
            if (yPos + OVAL_W > getHeight()) {
                yPos = 0;
            }
            repaint();
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame("SwingFoo01");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocationRelativeTo(null);
                frame.add(new SwingFoo01());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

}

Now, having said this, if any of your code is particularly long-running, something that requires many CPU cycles, then you would need to handle threading more directly, say with a SwingWorker, but even this could be paused in an event-driven way if necessary, without resorting to polling.

Upvotes: 1

Teddy
Teddy

Reputation: 4243

You are right.. it would be nice if a piece of code running in a Thread could be "regular" Java code, which is not thread-aware, and that code could be interrupted from outside when needed. But, it doesn't look like there is a good way to do this.

Few suggestions:

  1. I believe you don't need try/catch unless you are going to "sleep" or "wait". You just need to check for isInterrupted() more often, if possible.
  2. You could just have another thread, and a progress bar for the user to wait until the path-finder-thread stops. Show a spinner until the key thread responds, making it a better experience for the user.
  3. Real threads in Java do really get blocked for some cases like I/O. So the JVM can interrupt the thread only after the I/O block opens. Such cases may be better handled in Virtual Threads. Since the Virtual thread is parked, it may be able to respond to interrupt even while supposedly "blocked" on I/O. So, running the program on Java 21 and using new VirtualThread may make your program more responsive. Although, I doubt if this works as well when the workload is CPU bound.
  4. You could also make both the stopBot and isRunning methods to be synchronized. This would avoid a situation where code in one thread does not "see" the change made to that variable from the other thread, because these values are stored in CPU registers / caches and not flushed to main memory immediately.

Upvotes: 0

Related Questions