I'm debating trying to take a crack at getting something similar to C#'s async-co-routine like nature over on JavaFX.
This is technically feasible since the Quantem toolkit exposes the enterNestedEventLoop
and exitNestedEventLoop
methods, but using them is tricky.
If you haven't seen it, C#'s await & task syntax (butchered into Java), looks like this:
public Task<Project> doThing(int countOfSomethingImportant){ return Task.running(self -> { Project project = new Project(eventBus, serializer, graphModel); doInlineTaskThing(countOfSomethingImportant, project); String serialized = serializer.toXMLTask(project).await(); Project deserialized = serializer.fromXMLTask(serialized, Project.class).await(); return deserialized; });}
The idea is that this method can be called from the UI thread, and it's reasonably imperative, so we don't have to deal with the complexity of creating and managing Tasks and worker threads from business logic, we can just call await()
, on a task, which will block the current UI-thread-based job but not the entire UI thread itself.
As a prototype (untested, even functionally), I've come up with this:
class MyTask<T> extends javafx.concurrent.Task<T>{
//a number of other things, including wiring up progress bars
//and special cases for Void return types.
private @WrittenOnce boolean completed = false;
private final Queue<Runnable> completionJobs = new LinkedList<>();
private boolean addCompletionAction(Runnable onCompletion){
synchronized (this){
if( ! completed){
completionJobs.add(onCompletion);
return true;
}
else{
return false;
}
}
}
@Override public void done(){
synchronized (this){
completed = true;
}
completionJobs.forEach(Runnable::run);
}
public static final String SyncingFUBAR =
"we were interrupted while waiting for the signal that a nested event loop was entered. " +
"This (might?) mean that we've just entered a nested event loop we will never exit," +
"alternatively we might have just exited an event-loop we shouldn't have.";
public TResult await(){
try {
if(BootstrappingUtilities.isFXApplicationThread()){
CountDownLatch nestedLoopEnteredSignal = new CountDownLatch(1);
boolean completionRegistered = addCompletionAction(() -> {
logOnException(nestedLoopEnteredSignal::await, SyncingFUBAR, Log);
TResult rval = null;
try{
rval = get();
}
catch (InterruptedException | ExecutionException e) {
//should I suppress this?
throw new RuntimeException(e);
}
finally {
Toolkit.getToolkit().exitNestedEventLoop(this, rval);
}
});
if(completionRegistered){
assert BootstrappingUtilities.isFXApplicationThread();
//enqueing the signal *should* mean we dont have a race condition between count-down and nested-event-loop-enter
Platform.runLater(nestedLoopEnteredSignal::countDown);
return (TResult) Toolkit.getToolkit().enterNestedEventLoop(this);
}
else{
//the task has already completed.
return get();
}
}
else {
return get();
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
catch (CancellationException | ExecutionException e) {
throw new RuntimeException(e);
}
}
Regarding semantics:
- Is this sane?
- Is it worth pursuing the concept of a co-routine between UI thread and worker thread from Java?
- If it is, is it worth trying to formalize the two threads so I have a model-modifying thread and a ui-thread? If I can make this standard it might fix up a whole lot of kloogy model-currency-protection schemes
- What should I do if the UI thread is interrupted while I'm waiting for my latch?
Regarding syntax:
- Is there a way to use an
AtomicBoolean
(or something else?) instead of thatsynchronized(this) flag = whatever
nonsense? - Is there a better tool than a count-down latch?
Regarding testing:
- What's a good way to fuzz this?