mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 12:48:00 +00:00
Switch game thread executor to be a single-thread
executor instead, of a thread pool. This is needed because when things need to execute on the game thread, it's not thread safe so only one such thing should be executing at a time. This should fix ConcurrentModificationException from CardAreaPanel.selectCard() on mobile. This is a reland of r34680 with a fix for being able to run tasks on the game thread while waiting for input from the user. Specifically, tested paying costs and using the dev panel, but should work with anything that schedules tasks via GameAction.invoke().
This commit is contained in:
@@ -20,8 +20,10 @@ public class ThreadUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static ExecutorService gameThreadPool = Executors.newCachedThreadPool(new WorkerThreadFactory("Game"));
|
// Use a single game thread, rather than a pool of them, because when something
|
||||||
private static ExecutorService getGameThreadPool() { return gameThreadPool; }
|
// needs to execute on the game thread, it's because it's not thread safe.
|
||||||
|
private final static ExecutorService gameThread = Executors.newSingleThreadExecutor(new WorkerThreadFactory("Game"));
|
||||||
|
private static ExecutorService getGameThreadPool() { return gameThread; }
|
||||||
private final static ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2, new WorkerThreadFactory("Delayed"));
|
private final static ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2, new WorkerThreadFactory("Delayed"));
|
||||||
private static ScheduledExecutorService getScheduledPool() { return scheduledPool; }
|
private static ScheduledExecutorService getScheduledPool() { return scheduledPool; }
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package forge.game;
|
package forge.game;
|
||||||
|
|
||||||
|
import com.google.common.base.Function;
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
@@ -76,6 +77,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods for common actions performed during a game.
|
* Methods for common actions performed during a game.
|
||||||
@@ -1744,8 +1746,17 @@ public class GameAction {
|
|||||||
// state effects are checked only when someone gets priority
|
// state effects are checked only when someone gets priority
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invokes given runnable in Game thread pool - used to start game and perform actions from UI (when game-0 waits for input)
|
private Function<Runnable, Void> invokeFunction;
|
||||||
public void invoke(final Runnable proc) {
|
public synchronized void setInvokeFunction(Function<Runnable, Void> invokeFunction) {
|
||||||
|
this.invokeFunction = invokeFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invokes given runnable on the Game thread - used to start game and perform actions from UI when waiting for input.
|
||||||
|
public synchronized void invoke(final Runnable proc) {
|
||||||
|
if (invokeFunction != null) {
|
||||||
|
invokeFunction.apply(proc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (ThreadUtil.isGameThread()) {
|
if (ThreadUtil.isGameThread()) {
|
||||||
proc.run();
|
proc.run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
package forge.match.input;
|
package forge.match.input;
|
||||||
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.LinkedBlockingDeque;
|
||||||
|
|
||||||
|
import com.google.common.base.Function;
|
||||||
import forge.FThreads;
|
import forge.FThreads;
|
||||||
import forge.error.BugReporter;
|
import forge.error.BugReporter;
|
||||||
import forge.player.PlayerControllerHuman;
|
import forge.player.PlayerControllerHuman;
|
||||||
|
import forge.util.ThreadUtil;
|
||||||
|
|
||||||
public abstract class InputSyncronizedBase extends InputBase implements InputSynchronized {
|
public abstract class InputSyncronizedBase extends InputBase implements InputSynchronized {
|
||||||
private static final long serialVersionUID = 8756177361251703052L;
|
private static final long serialVersionUID = 8756177361251703052L;
|
||||||
private final CountDownLatch cdlDone;
|
private static final Runnable terminationMarker = new Runnable() { public void run() { } };
|
||||||
|
|
||||||
|
// The gameTaskQueue indicates tasks to run while blocked on the game. To stop, add terminationMarker (as null is
|
||||||
|
// not allowed).
|
||||||
|
private LinkedBlockingDeque<Runnable> gameTaskQueue = new LinkedBlockingDeque<Runnable>();
|
||||||
|
|
||||||
public InputSyncronizedBase(final PlayerControllerHuman controller) {
|
public InputSyncronizedBase(final PlayerControllerHuman controller) {
|
||||||
super(controller);
|
super(controller);
|
||||||
cdlDone = new CountDownLatch(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void awaitLatchRelease() {
|
public void awaitLatchRelease() {
|
||||||
FThreads.assertExecutedByEdt(false);
|
FThreads.assertExecutedByEdt(false);
|
||||||
try{
|
try {
|
||||||
cdlDone.await();
|
Runnable r = gameTaskQueue.take();
|
||||||
|
while (r != terminationMarker) {
|
||||||
|
r.run();
|
||||||
|
r = gameTaskQueue.take();
|
||||||
|
}
|
||||||
} catch (final InterruptedException e) {
|
} catch (final InterruptedException e) {
|
||||||
BugReporter.reportException(e);
|
BugReporter.reportException(e);
|
||||||
}
|
}
|
||||||
@@ -27,12 +36,34 @@ public abstract class InputSyncronizedBase extends InputBase implements InputSyn
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final void relaseLatchWhenGameIsOver() {
|
public final void relaseLatchWhenGameIsOver() {
|
||||||
cdlDone.countDown();
|
gameTaskQueue.add(terminationMarker);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showAndWait() {
|
public void showAndWait() {
|
||||||
|
final boolean isGameThread = ThreadUtil.isGameThread();
|
||||||
|
|
||||||
|
if (isGameThread) {
|
||||||
|
// If we're on the game thread, redirect the "run on game thread" function to instead go through us.
|
||||||
|
getController().getGame().getAction().setInvokeFunction(new Function<Runnable, Void>() {
|
||||||
|
public Void apply(Runnable r) {
|
||||||
|
gameTaskQueue.add(r);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getController().getInputQueue().setInput(this);
|
getController().getInputQueue().setInput(this);
|
||||||
awaitLatchRelease();
|
awaitLatchRelease();
|
||||||
|
|
||||||
|
if (isGameThread) {
|
||||||
|
// Reset the invoke function to null.
|
||||||
|
getController().getGame().getAction().setInvokeFunction(null);
|
||||||
|
// There's a race that a new task may be queued up before we've reset the invoke function.
|
||||||
|
// To handle this, schedule any remaining tasks normally.
|
||||||
|
for (Runnable r : gameTaskQueue) {
|
||||||
|
getController().getGame().getAction().invoke(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final void stop() {
|
protected final void stop() {
|
||||||
@@ -50,7 +81,7 @@ public abstract class InputSyncronizedBase extends InputBase implements InputSyn
|
|||||||
if (getController().getInputQueue().getInput() != null) {
|
if (getController().getInputQueue().getInput() != null) {
|
||||||
getController().getInputQueue().removeInput(InputSyncronizedBase.this);
|
getController().getInputQueue().removeInput(InputSyncronizedBase.this);
|
||||||
}
|
}
|
||||||
cdlDone.countDown();
|
gameTaskQueue.add(terminationMarker);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onStop() { }
|
protected void onStop() { }
|
||||||
|
|||||||
Reference in New Issue
Block a user