Concurrency infrastructure
One of the major challenges of a complex system is to remain responsive while tasks are being performed. This
challenge is even greater in an extensible system, when components that weren't designed to run together are
sharing the same resources. The
org.eclipse.core.runtime.jobs
package addresses this challenge by providing infrastructure for scheduling, executing, and managing
concurrently running operations. This infrastructure is based on the use of jobs to represent
a unit of work that can run asynchronously.
Jobs
The
Job
class represents
a unit of asynchronous work running concurrently with other jobs. To perform a task, a plug-in creates
a job and then
schedules it. Once a job is scheduled, it is added to a job queue managed by the
platform. The platform uses a background scheduling thread to manage all of the pending jobs. As a running
job completes, it is removed from the queue and the platform decides which job to run next. When a job
becomes active, the platform invokes its
run() method. Jobs are best demonstrated with a simple example:
class TrivialJob extends Job {
public TrivialJob() {
super("Trivial Job");
}
public IStatus run(IProgressMonitor monitor) {
System.out.println("This is a job");
return Status.OK_STATUS;
}
}
The job is created and scheduled in the following snippet:
TrivialJob job = new TrivialJob();
System.out.println("About to schedule a job");
job.schedule();
System.out.println("Finished scheduling a job");
The output of this program is timing dependent. That is, there is no way
to be sure when the job's
run method will execute in relation
to the thread that created the job and scheduled it. The output will
either be:
About to schedule a job
This is a job
Finished scheduling a job
or:
About to schedule a job
Finished scheduling a job
This is a job
If you want to be certain that a job has completed before continuing, you can use
the join() method. This method will block the caller until the job has completed, or
until the calling thread is interrupted. Let's rewrite our snippet from above in a more
deterministic manner:
TrivialJob job = new TrivialJob();
System.out.println("About to schedule a job");
job.schedule();
job.join();
if (job.getResult().isOk())
System.out.println("Job completed with success");
else
System.out.println("Job did not complete successfully");
Assuming the
join() call is not interrupted, this method is guaranteed to
return the following result:
About to schedule a job
This is a job
Job completed with success
Of course, it is generally not useful to join a job immediately after scheduling it,
since you obtain no concurrency by doing so. In this case you might as well do the work
from the job's run method directly in the calling thread. We'll look at some examples later on where
the use of join makes more sense.
The last snippet also makes use of the job result. The result
is the
IStatus
object that is returned from the job's run() method. You can use this result
to pass any necessary objects back from the job's run method. The result can also be used to
indicate failure (by returning an
IStatus
with severity IStatus.ERROR), or cancellation (IStatus.CANCEL).
Common job operations
We've seen how to schedule a job and wait for it complete, but there are many other
interesting things you can to do jobs. If you
schedule a job but then decide it is no longer needed, the job can be stopped using
the cancel() method. If the job has not yet started running when canceled,
the job is immediately discarded and will not run. If, on the other hand, the job
has already started running, it is up to the job whether it wants to respond to the cancellation.
When you are trying to cancel a job, waiting for it using the join() method comes in handy.
Here is a common idiom for canceling a job, and waiting until the job is finished before
proceeding:
if (!job.cancel())
job.join();
If the cancellation does not take effect immediately, then cancel() will return
false and the caller will use join() to wait for the job to successfully cancel.
Slightly less drastic than cancellation is the sleep() method. Again,
if the job has not yet started running, this method will cause the
job to be put on hold indefinitely. The job will still be remembered by the platform,
and a wakeUp() call will cause the job to be added to the wait
queue where it will eventually be executed.
Job states
A job goes through several states during its lifetime. Not only can it be manipulated through API
such as cancel() and sleep(), but its state also changes as the platform runs and completes the
job. Jobs can move through the following states:
-
WAITING indicates that the job been scheduled to run, but is not running yet.
-
RUNNING indicates that the job is running.
-
SLEEPING indicates that the job is sleeping due to a sleep request or because it was scheduled
to run after a certain delay.
-
NONE indicates that the job is not waiting, running, or sleeping. A job is in this state when
it has been created but is not yet scheduled. It is also in this state after it is finished running or when
it has been canceled.
A job can only be put to sleep if it is currently WAITING. Waking up a sleeping job will put it back in the
WAITING state. Canceling a job will return it to the NONE state.
If your plug-in needs to know the state of a particular job, it can register a job change listener that is
notified as the job moves through its life-cycle. This is useful for showing progress or otherwise reporting on
a job.
Job change listeners
The
Job
method
addJobChangeListener can be used to register a listener on a particular job.
IJobChangeListener
defines protocol for responding to the state changes in a job:
-
aboutToRun is sent when the job is about to be run.
-
awake is sent when a previously sleeping job is now waiting to be run.
-
done is sent when a job finishes execution.
-
running is sent when a job starts running.
-
scheduled is sent when a job is scheduled and waiting in the queue of jobs.
-
sleeping is sent when a waiting job is put to sleep.
In all of these cases, the listener is provided with an
IJobChangeEvent
that specifies the job undergoing the state change and status on its completion (if it is done).
Note: Jobs also define the getState() method for obtaining the (relatively) current
state of a job. However, this result is not always reliable since jobs run in a different thread and may
change state again by the time the call returns. Job change listeners are the recommended mechanism
for discovering state changes in a job.
The job manager
IJobManager
defines protocol for working with all of the jobs in the system. Plug-ins that show progress or otherwise
work with the job infrastructure can use
IJobManager
to perform tasks such as suspending all jobs in the system, finding out which job is running, or receiving progress
feedback about a particular job. The platform's job manager can be obtained using
Platform
API:
IJobManager jobMan = Platform.getJobManager();
Plug-ins interested in the state of all jobs in the system can register a job change listener on the job manager rather
than registering listeners on many individual jobs.
Job families
It is sometimes easier for a plug-in to work with a group of related jobs as a single unit.
This can be accomplished using job families. A job declares that it belongs to a certain family by
overriding the belongsTo method:
public static final String MY_FAMILY = "myJobFamily";
...
class FamilyJob extends Job {
...
public boolean belongsTo(Object family) {
return family == MY_FAMILY;
}
}
IJobManager
protocol can be used to cancel, join, sleep, or find all jobs in a family:
IJobManager jobMan = Platform.getJobManager();
jobMan.cancel(MY_FAMILY);
jobMan.join(MY_FAMILY, null);
Since job families are represented using arbitrary objects, you can store interesting
state in the job family itself, and jobs can dynamically build family objects as needed.
It is important to use family objects that are
fairly unique, to avoid accidental interaction with the families created by
other plug-ins.
Families are also a convenient way of locating groups of jobs. The method IJobManager.find(Object family) can
be used to locate instances of all running, waiting, and sleeping jobs at any
given time.