1
\$\begingroup\$

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 that synchronized(this) flag = whatever nonsense?
  • Is there a better tool than a count-down latch?

Regarding testing:

  • What's a good way to fuzz this?
\$\endgroup\$
1
\$\begingroup\$

Note: this is mostly a conventions review.

I find formatting a bit hard to read. Some points:

class MyTask<T> extends javafx.concurrent.Task<T>{

Add a space before the brace:

class MyTask<T> extends javafx.concurrent.Task<T> {

Same with the following lines:

private boolean addCompletionAction(Runnable onCompletion){
...
    synchronized (this){
...
public TResult await(){

and other lines, but they have more than this problem, so I will address that separately.


//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<>();

End-of-line comments usually have a space between // and the comment itself:

// 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<>();

Also, the extra spaces you added for line three of that snipped can be removed without affecting readability:

// 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<>();

Inside synchronized (this):

        if( ! completed){
            completionJobs.add(onCompletion);
            return true;
        }
        else{
            return false;
        }

A couple of problems:

Line 1: Space between if and (
Line 1: No extra spaces before and after !
Line 1: Space before brace

Lines 4-5: Should be combined
Line 5: Space before brace

Result:

        if (!completed) {
            completionJobs.add(onCompletion);
            return true;
        } else {
            return false;
        }

In addition to formatting, you can safely omit the else, as the if part returns anyways:

        if (!completed) {
            completionJobs.add(onCompletion);
            return true;
        }
        return false;

}
@Override public void done(){
    synchronized (this){
        completed = true;
    }

    completionJobs.forEach(Runnable::run);
}

This is the end brace of the previous method plus the new method.

Again, a couple of things:

Lines 1-2: Extra newline between
Line 2: Should be separated into two lines: @Override and declaration
Line 2: Space before brace

Line 3: Already mentioned, space before brace Line 6: Extra newline serves no purpose, but can be left there if wanted

Result:

}

@Override
public void done() {
    synchronized (this) {
        completed = true;
    }
    completionJobs.forEach(Runnable::run);
}

    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);
    }

A couple of problems:

Line 2: Space between if and (
Line 2: Space before brace

Lines 3, 5, 7, and 9: Newline can be removed but may be left in wanted

Line 11: Space before brace

Lines 13-14: Should be merged

Line 15: Space between // and comment

Lines 17-18: Should be merged

Line 23: Space between if and (
Line 23: Space before brace

Line 25: Space between // and comment Line 25: typo, I assume? dont -> don't

Line 28-29: Should be merged

Line 30: Space between // and comment

Line 33-34: Should be merged

Line 37-38: Should be merged

Line 41-42: Should be merged

Result:

    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 don't 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);
    }

One other point:

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.";

Actually, two things:

  1. Code that form the same command but are on a different line should be 8-spaced, or 2-tabbed.
  2. Java Naming Conventions specify that static final variable should be ALL_CAPS_WITH_UNDERSCORE.

Result:

public static final String SYNCING_FUBAR =
        "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.";
\$\endgroup\$
  • 1
    \$\begingroup\$ well thanks for your detailed thoughts, out of curiosity is this strict K&R C conventions or is there a java equivalent? We do have a formatter on our project that I haven't run. Interestingly, the one major gripe I've had with all of these conventions has been around the bang-operator. Ive personally fixed three or four bugs relating to a bang that was simply incorrect, and hidden because its a small and unnoticable (and not strongly highlighted in intelli) character. My convention is now to put a space before and after, but some people on my current project write !!!expression \$\endgroup\$ – Groostav Nov 27 '15 at 5:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.