My app has background threads that need to access the UI. Imagine a chess program (AI) that "thinks" for a number of seconds before it plays a move on the board. While the thread runs the UI is blocked for input but there is still output.
There are 3 threads involved:
During the search (invokeAndBlock) the stopButton is accessible to force the search to stop (not shown).
Below is my current implementation. It works and my question is: is it the right way to implement this?
(I read https://www.codenameone.com/blog/callserially-the-edt-invokeandblock-part-1.html and part-2.)
Form mainForm;
TextField whiteTime, blackTime; // updated by clock thread
TextField searchInfo; // updated by think thread
Clock clock;
Move move;
public void start() {
...
mainForm = new Form(...);
...
thinkButton.addActionListener((ActionListener) (ActionEvent evt) -> {
think();
});
mainForm.show();
}
void think() {
blockUI(); // disable buttons except stopButton
clock.start(board.player); // this thread calls showWhiteTime or showBlackTime every second
invokeAndBlock(() -> { // off the EDT
move = search(board, time); // e.g. for 10 seconds
});
clock.stop();
animateMove(board, move);
clock.start(board.player);
freeUI();
}
// search for a move to play
Move search(Board board, int time) {
...
while (time > 0) {
...
showSearchInfo(info); // called say a few times per second
}
return move;
}
void showSearchInfo(String s) { // access UI off the EDT
callSerially(() -> { // callSerially is necessary here
searchInfo.setText(s);
});
}
void showWhiteTime(String s) {
whiteTime.setText(s); // no callSerially needed, although off the EDT (?)
}
void showBlackTime(String s) {
blackTime.setText(s); // no callSerially needed, although off the EDT (?)
}
Edit: new versions of think, showWhiteTime and showBlackTime.
// version 2, replaced invokeAndBlock by Thread.start() and callSerially
void think() {
blockUI(); // disable buttons except stopButton
new Thread(() -> { // off the EDT
clock.start(board.player); // this thread calls showWhiteTime or showBlackTime every second
move = search(board, time); // e.g. for 10 seconds
clock.stop();
callSerially(() -> {
animateMove(board, move);
clock.start(board.player);
freeUI();
});
}).start();
}
// version 2, added callSerially
void showWhiteTime(String s) { // access UI off the EDT
callSerially(() -> {
whiteTime.setText(s);
});
}
// version 2, added callSerially
void showBlackTime(String s) { // access UI off the EDT
callSerially(() -> {
blackTime.setText(s);
});
}
Most of the code is fine though I would avoid the EDT violations you have in showWhiteTime
and showBlackTime
. EDT violations can fail in odd ways all of a sudden since you trigger async operations and things can turn nasty quickly. I suggest turning on the EDT violation detection tool in the simulator.
Two things to keep in mind when using invokeAndBlock
:
The second point is a difficult one to grasp and a source of many mistakes so it's worth explaining a bit.
consider this code:
buttonA.addActionListener(e -> {
doStuff();
invokeAndBlock(...);
doOtherStuff();
});
buttonA.addActionListener(e -> doSomethingImportant());
That might not seem realistic as you usually don't add two separate listeners one after the other but this happens enough e.g. if one change triggers another etc.
The current event processing will be blocked for buttonA
during invokeAndBlock
. That means that doOtherStuff()
will wait for the invokeAndBlock
and also doSomethingImportant()
will wait.
If doSomethingImportant()
shows another form you can end up with weird behavior such as ages after you pressed the button and did a lot of other things suddenly your form changes.
So you need to be very conscious of your usage of invokeAndBlock
.