Now, for those of you not familiar with "concurrency" in programming, it means when multiple parts of a program run at the same time, "competing for the computer's resources" like CPU or RAM. Concurrency is hard for three reasons:
- you have to control what happens when, because the classical "everything happens in a fixed sequence" is not true
- speaking of "competing for resources", when two concurrent threads of execution access the same data, they can overwrite each other, messing things up
- for this reason, locks and synchronizing have come up, which grant exclusive access to a resource to a single thread. This also means that threads have to wait for resources until they aren't locked any more. In the worst case, two threads can wait for each other.
So there are two threads, which means every GUI interaction means synchronization. The first GUI interaction implemented was playing cards and activating abilities. The code to enable that was pretty wordy and hardly extensible:
private boolean ready; private PlayAction a; public GuiActor(Player player) { super(player); }
public synchronized void putAction(PlayAction a) { ready = true; this.a = a; notifyAll(); } public synchronized PlayAction getAction() { ready = false; while(!ready) try { wait(); } catch(InterruptedException ex) { ex.printStackTrace(); } return a; }
On the surface, it doesn't look that bad, but it has several issues:
- It is low-level: It handles all the synchronization stuff itself, as seen by boolean ready, notifyAll(), synchronized, while(!ready) ... This is all stuff that has to be repeated for every sort of user input: declare attacker, blocker, ...
- It moves control away from the actor: putAction takes a PlayAction, which is the result of e.g. clicking on a card. How that click is associated to the PlayAction is not controlled by the actor which violates atomicity
- Translating click to PlayAction is specific to playing spells and not compatible with e.g. declaring attackers. This means that the translation, which is not atomic, has to be performed for every type of input
- Missing Encapsulation: The interpretation as a PlayAction is only valid when the player has priority, and this fact is not represented here: There is no representation of the fact whether the Actor is waiting for a PlayAction or for declaring an attacker
public class GuiMagicActor extends AbstractMagicActor {
public final GuiChannels channels = new GuiChannels();
private static
a.start();
log.debug("Waiting for result...");
T result = Parallel.getValue(f, ch);
log.debug("received!");
a.dispose();
return result;
}
public PlayAction getAction() {
return getValue(channels.fiber, channels.actions, new ActionActor(this));
}
...
}
This new version is based on the Jetlang concurrency framework, which works using channels and callbacks: Messages published to a channel are - asynchronously - forwarded to registered callbacks, which may then in turn publish new messages. The Parallel.getValue() method is used to get a value back from the channel synchonously, and the ActionActor encapsulates the state, in this case choosing a PlayAction to perform:
public class GuiChannels {
/**
* Channel for receiving {@link PlayAction}s to execute when the player has priority
*/
public final Channel
/**
* Channel for publishing {@link MagicObject}s when the user clicks on them
*/
public final Channel
/**
* Channel for publishing {@link Player}s when the user clicks on them
*/
public final Channel
/**
* Channel for publishing when the user clicks "pass priority"
*/
public final Channel
public final Fiber fiber;
public GuiChannels() {
PoolFiberFactory f = new PoolFiberFactory(Executors.newCachedThreadPool());
fiber = start(f.create());
}
private Fiber start(Fiber f) {
f.start();
return f;
}
}
public class ActionActor extends GuiActor {
public ActionActor(GuiMagicActor actor) {
super(actor);
}
@Override
public void start() {
disposables.add(actor.channels.objects.subscribe(actor.channels.fiber, new CardCallback()));
disposables.add(actor.channels.passPriority.subscribe(actor.channels.fiber, new PassPriorityCallback()));
}
private class CardCallback implements Callback
@Override
public void onMessage(MagicObject c) {
log.debug("Received: " + c);
PlayAction a = GuiUtil.getActionOptional(actor.getPlayer(), c);
if(a != null) actor.channels.actions.publish(a);
}
}
private class PassPriorityCallback implements Callback
@Override
public void onMessage(Void v) {
log.debug("Received pass priority");
actor.channels.actions.publish(null);
}
}
}
The ActionActor receives Messages through the objects and passPriority channels and reacts specific to its task.
Hope you liked it!
No comments:
Post a Comment