In a post from earlier this year I explored the concept of background tasks in JavaFX. If you haven’t yet, you might want to review that article before reading on. For the impatient: JavaFX is currently a single threaded programming language. All code you write in JavaFX occurs on the same thread. Since you don’t want to write an unresponsive application, you need to write long-lived operations on a background thread. The Task API provides a single consistent abstraction which all background operations in JavaFX are based on. This means whether you are computing fibonacci sequences, breaking down protein chains, writing to a database or reading data from disk there is a single consistent programming model and API that your GUI communicates with. And we think this is a pretty good idea.
JavaFX Script has to compile on desktop, mobile, and tv platforms. This puts some constraints on what Java API we can use for the core JavaFX platform.
The real RunnableFuture class in the java concurrent utils is one of these classes that we couldn’t use because it wasn’t available on all platforms, yet. So we wanted to use essentially the same interface, but needed to put it into the javafx namespace.
That is why we added a RunnableFuture interface to the javafx.async package. However, somehow along the way it got whacked and ended up looking just like Runnable. Doh! This will be fixed in the next release.
Because each background task is encapsulated in a Task subclass of some kind, it also provides a convenient programming model for third party developers to build libraries of background tasks. For example, I expect third party libraries like jfxtras will probably provide several reusable tasks such as for reading lines of text from files, saving data to disk, executing SQL queries, or other such operations. You will likely want to create your own set of such background tasks as you commonly encounter to keep handy in your own toolbox.
Because we require all background work to be done in a background Java thread, we have provided a helper class called JavaTaskBase from which you should extend to create custom background tasks. Unfortunately we made one small (and rather embarrassing) error in the 1.2 release which, while it will not impact your code much when you upgrade to 1.3, you should be aware of (see sidebar for details). We would also like to make the JavaTaskBase more convenient for you to use, although what we have currently I found rather quite friendly and helped me write threaded code rather cleanly.
JavaTaskBase
The Task class contains many different lifecycle variables, events, and functions. When you create a JavaTaskBase subclass, you get all of this for free. Your main responsibility is simply to define the parameters to the background thread, the results of the background computation, and the Java implementation that will carry out the work on the background thread.
Step 1: Define the input parameters
Let’s take as our example a simple Task implementation which reads the contents of a File from the file system and returns the entire contents as a single String. What would we need to define as the input parameters? For the simple case, it is probably sufficient to simply provide a path to the file as the input parameter. For the sake of this example, we’ll assume that this Task accesses the local filesystem only.
[jfx]
public class LocalFileReader extends JavaTaskBase {
public-init var path:String;
}
[/jfx]
In the above code, we defined the path to the file as a public-init var
. This means that it is a variable that can only be set by external code during init time — essentially rendering this variable immutable. This is an important key point about Tasks — they are designed to be used once and then thrown away, not reused. Create a new instance whenever you need to perform a new task.
Step 2: Define the results
Now that we have defined our input parameter, we should add to our class whatever output values, event handlers, or other API that the user would use during the course of this operation, or at its completion. In the case of our file reader, we are going to provide a single String variable which will contain the entire contents of the file as a single String. This is obviously a contrived example as it would only be useful with very small files, but should demonstrate many of the issues in building a basic Task implementation.
[jfx]
public class LocalFileReader extends JavaTaskBase {
public-init var path:String;
/**
* The contents of the file as a String. This is populated upon successful
* completion of the background task.
*/
public-read var contents:String;
}
[/jfx]
The contents
variable is defined as public-read var
, meaning that it is publicly readable but not publicly settable. The LocalFileReader class itself can write to the variable (which is a good thing or it could never be set!), but no other code can.
Step 3: Define the Java implementation
We are now ready to write the Java implementation of the background task. To do this we simply need to create a Java class which implements RunnableFuture (see sidebar for conversation). The Java implementation will generally also be an immutable object, and in our case, that is exactly what we are going to do.
public class LocalFileReaderImpl implements RunnableFuture { /** The path to the file to read */ private String path; /** The buffer to contain the contents of the file */ private StringBuffer contents; public LocalFileReaderImpl(String path) { this.path = path; this.contents = new StringBuffer(); } /** * All code executed in the run() method is done so on a background thread * and can happily take as long as it likes. Exceptions that occur will be * passed along to users of your Task for you */ public void run() throws Exception { FileInputStream in = new FileInputStream(path); try { BufferedInputStream bin = new BufferedInputStream(in); String line = null; while ((line = bin.readLine()) != null) { contents.append(line); contents.append("\n"); } } finally { try { in.close(); } catch (Exception e) { } } } }
The run
method executes on the background thread. JavaFX will make sure this happens, reusing threads in a thread pool and so forth. For advanced users, if you absolutely must manage your own thread pools or concurrency on a very fine-grained level, you will probably want to create your own JavaTaskBase equivalent which extends directly from Task.
At the conclusion of the run
method, the contents buffer has been completely populated with the file contents and the input stream has been closed. Now we must add a single method to our LocalFileReader which will create and return the LocalFileReaderImpl so that JavaFX knows what Java code to call when it comes time to call it on a background thread.
[jfx]
public class LocalFileReader extends JavaTaskBase {
//.ā¦ other code goes here as defined in the previous examples ā¦.
/** save a reference to this, we’ll need it later */
var impl:LocalFileReaderImpl;
/**
* Create and return an instance of the imp class. This is called
* when the task is started.
*/
override function create() { impl = new LocalFileReaderImpl(path) }
}
[/jfx]
We now have created our Task and its Java implementation. However there is one big problem remaining, which is that when the task completes, our LocalFileReader has no way of knowing that this has occurred. We will add some convenience API to JavaTaskBase to make this a bit easier, but for the moment we can solve the problem by simply introducing a callback interface. Our LocalFileReader will implement this interface and the LocalFileReaderImpl will invoke the “finished” function of this interface when the background task completes. The LocalFileReader can then read the finished contents buffer from the impl class and set its own contents
variable with the final result. The complete source code for this example is found below.
public interface FinishedHandler { public void finished(); }
Now have LocalFileReader implement this interface and pass a reference of “this” down to the imp:
[jfx]
public class LocalFileReader extends JavaTaskBase, FinishedHandler {
/** A path to a file on the local file system */
public-init var path:String;
/**
* The contents of the file as a String. This is populated upon successful
* completion of the background task.
*/
public-read var contents:String;
/** save a reference to this, we’ll need it later */
var impl:LocalFileReaderImpl;
/**
* Create and return an instance of the imp class. This is called
* when the task is started.
*/
override function create() { impl = new LocalFileReaderImpl(this, path) }
/**
* This is called by the imp while still on the background thread so our
* very first responsibility is to call FX.deferAction, which will execute the
* supplied anonymous function on the FX event thread
*/
override function finished() {
FX.deferAction(function() {
contents = impl.contents.toString();
});
}
}
[/jfx]
public class LocalFileReaderImpl implements RunnableFuture { /** The handler to call when we have finished reading from the file */ private FinishedHandler handler; /** The path to the file to read */ private String path; /** The buffer to contain the contents of the file */ private StringBuffer contents; public LocalFileReaderImpl(FinishedHandler handler, String path) { this.handler = handler; this.path = path; this.contents = new StringBuffer(); } /** * All code executed in the run() method is done so on a background thread * and can happily take as long as it likes. Exceptions that occur will be * passed along to users of your Task for you */ public void run() throws Exception { FileInputStream in = new FileInputStream(path); try { BufferedInputStream bin = new BufferedInputStream(in); String line = null; while ((line = bin.readLine()) != null) { contents.append(line); contents.append("\n"); } handler.finished(); } finally { try { in.close(); } catch (Exception e) { } } } }
There are loads of variations that you can make as suits your task. For example, you could establish a long running background task with job queues, callbacks, and more complicated state management (extra bonus props for the first person to implement a Python or Ruby like file read operation conveniently in FX). You can execute database calls or web service calls or other such long running tasks.
In the future we will add more convenience variables to JavaTaskBase, and maybe provide an implementation base class as well to help ease things. For example it would be nice to do away with the custom FinishedHandler and instead to have JavaTaskBase automatically realize when a task has finished and invoke an overridden function in the sub class that does the synchronization. In the future we hope to add threading support tightly into the language itself which will make a lot of this easier. While there are a number of steps involved, there is not much code involved and it is fairly straightforward — as far as threading code goes that is!
Very nice explanation!
Just one note:
The FX.deferAction is *very* important but could easily be missed. If you’d omit the FX.deferAction it would still compile and would probably work. Well in 95% of the time š Unless you’re unlucky (like me) and you’re running into deadlock situations when the background Thread and the main Event-Thread try to modify the same data at the same time š
It happend to me and my application just sporadically hanged: http://forums.sun.com/thread.jspa?forumID=934&threadID=5413671
Not very funny š
Of course FX.deferAction absolulty makes sense once you understand that there are actually 2 threads at work but it can be all to easily be forgotten to add.
Are there any plans to detect or force the use of it?
I think if the callback system was built into JavaTaskBase (ie: the finished function was built into JavaTaskBase), then we could ensure it is always on the right thread. If you try to provide JavaTaskBase a RunnableFuture implementation that is an FX object then it will complain.
Thanks for this very helpful tutorial!
I’ve been looking for ways to get around the “lock-up” in the GUI when waiting for long operations to complete from a server. Looks like integrating JavaTaskBase was the way back to responsiveness.
As a complete newbie on this, I also learned that the start() method should be called on LocalFileReaderImpl to create the separate thread for execution. I’m looking forward to what’s new in 1.3 but am happy to know that I can start using JavaTaskBase right away!