This tutorial describes how to do asynchronous work within Eclipse plug-ins and RCP applications via the Jobs API.
1. Prerequisites for this tutorial
This tutorial assumes that you have basic understanding of development for the Eclipse platform. Please see Eclipse RCP Tutorial or Eclipse plug-in Tutorial if you need any basic information.
2. Eclipse background processing
2.1. Main thread
An Eclipse RCP application runs in one process. By default, the Eclipse framework uses a single thread to run all the code instructions.
This thread runs the event loop for the application. It is the only thread that is allowed to interact with the user interface (UI). It is called the main thread. Sometimes it is called the UI thread, but this is a misnomer because it handles all events, not just UI events.
If another thread tries to update the UI, the Eclipse framework throws an SWTException exception.
org.eclipse.swt.SWTException: Invalid thread access
All events in the user interface are executed one after another. If you perform a long-running operation in the main thread, the application does not respond to user interaction during the execution time of this operation.
Blocking the user interaction is considered a bad practice. Therefore, it is important to perform all long-running operations in a separate thread. Long-running operations are, for example, network or file access.
As only the main thread is allowed to modify the user interface, the Eclipse framework provides ways for a thread to synchronize itself with the main thread. It provides the Eclipse Jobs framework, which allows you to run operations in the background and provides feedback on the job status to the Eclipse platform.
2.2. Using dependency injection and UISynchronize
The org.eclipse.e4.ui.di plug-in contains the UISynchronize class.
An instance of this class can be injected into an Eclipse application via dependency injection.
UISynchronize provides the syncExec() and asyncExec() methods to synchronize with the main thread.
2.3. Eclipse Jobs API
The Eclipse Jobs API provides support for running background processes and providing feedback about the progress of the Job.
The important parts of the Job API are:
-
IJobManager - schedules jobs
-
Job - the individual task to perform
-
IProgressMonitor - interface to communicate information about the status of your Job.
The static Job.create() method creates a job based on an ICoreRunnable lambda expression, which is the most concise way to define a job.
The creation and scheduling of a Job is demonstrated in the following code snippet.
// get UISynchronize injected as a field
@Inject
UISynchronize sync;
// more code
Job job = Job.create("Update table", (ICoreRunnable) monitor -> {
// do the long-running work here
// ...
// use UISynchronize to update the user interface
sync.asyncExec(() -> {
// do something in the user interface,
// e.g., set a text field
});
});
// start the job
job.schedule();
If you want to update the user interface from a Job, you need to synchronize the corresponding action with the user interface similar to the direct usage of threads.
2.4. Priorities of Jobs
You can set the Job priority via the job.setPriority() method.
The Job class contains predefined priorities, e.g. Job.SHORT, Job.LONG, Job.BUILD and Job.DECORATE.
The Eclipse job scheduler will use these priorities to determine the order in which the Jobs are scheduled.
For example, jobs with the priority Job.SHORT are scheduled before jobs with the Job.LONG priority.
Check the JavaDoc of the Job class for details.
2.5. Blocking the UI and providing feedback
Sometimes you want to change the cursor to give the user the feedback that something is running.
The easiest way to provide feedback is to change the cursor via the BusyIndicator.showWhile() method.
// show a busy indicator while the runnable is executed
BusyIndicator.showWhile(display, () -> {
// ... perform the work here ...
});
If this code is executed, the cursor changes to a busy indicator until the runnable is done.
3. Reporting progress
3.1. IProgressMonitor and the SubMonitor
An instance of IProgressMonitor is passed to every job and is used to report the progress of the job.
It is good practice to always convert this monitor to a SubMonitor via the SubMonitor.convert() method.
This call sets the total number of work units and provides a consistent API for reporting the progress of the main process and of child processes.
To report progress from methods called inside the job, pass a child monitor created via the split() method instead of the SubMonitor itself.
List<Task> tasks = taskService.getTasks();
Job job = Job.create("Process tasks", (ICoreRunnable) monitor -> {
// convert to SubMonitor and set the total number of work units
SubMonitor subMonitor = SubMonitor.convert(monitor, tasks.size());
for (Task task : tasks) {
// set the name of the current work
subMonitor.setTaskName("Working on task " + task.getSummary());
// workOnTask is a method in this class which does the actual work,
// pass a child monitor with a total work of 1 to it
workOnTask(task, subMonitor.split(1));
}
});
job.schedule();
When using a SubMonitor it is not necessary to call the beginTask() method, as SubMonitor.convert() calls it implicitly.
It is also not necessary to call done() inside the job, because the Jobs framework calls done() on the monitor it passed to the job after the job has finished.
|
A |
3.2. Taking conditions during progress into account
In some cases, the amount of work depends on conditions, which should be reported properly.
This can be done by using the setWorkRemaining() method of the SubMonitor.
Job job = Job.create("Process task", (ICoreRunnable) monitor -> {
// convert to SubMonitor and set the total number of work units
SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
if (taskNeedsPreconfiguration(task)) {
// takes 30 % of the work
preConfigureTask(task, subMonitor.split(30));
}
// ensure that only 70 % of the work remains
subMonitor.setWorkRemaining(70);
// do the rest of the work
workOnTask(task, subMonitor.split(70));
});
job.schedule();
In case the code in the taskNeedsPreconfiguration() if block is run, the setWorkRemaining() method does nothing.
Only if the code in the if block is skipped does it ensure that progress is reported properly.
Another use case for setWorkRemaining() is when the actual amount of work is determined later.
See the workOnTask method in the following snippet.
private void preConfigureTask(Task task, IProgressMonitor monitor) {
SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
subMonitor.setTaskName("Preconfiguring task " + task.getSummary());
// ... do the configuration
}
private void workOnTask(Task task, IProgressMonitor monitor) {
SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
subMonitor.setTaskName("Working on task " + task.getSummary());
// the loop over the related tasks is supposed to do 80 % of the work
SubMonitor loopMonitor = subMonitor.split(80);
// get the related tasks from a service
List<Task> relatedTasks = taskService.findRelatedTasks(task);
// the actual amount of work is only known at this point
loopMonitor.setWorkRemaining(relatedTasks.size());
for (Task relatedTask : relatedTasks) {
preConfigureTask(relatedTask, loopMonitor.split(1));
}
// ... do the work on the actual task with the remaining 20 %
doWorkOnActualTask(task, subMonitor.split(20));
}
For the loop in workOnTask a new child SubMonitor, which is supposed to do 80 % of the work, is created.
The actual remaining work for this SubMonitor is set later via setWorkRemaining(), once the number of related tasks is known.
3.3. Reporting progress in Eclipse RCP applications
In an Eclipse RCP application you decide how job progress is shown to the user.
For example, you can add a tool control with a ProgressBar to a toolbar in your application model.
By registering a ProgressProvider with the job manager you define which IProgressMonitor is handed to the scheduled jobs.
The following example creates a new monitor for every job and reports all progress to the shared progress bar.
The done() implementation is essential: it reports the work the job did not report itself, for example, the work assigned to the last child monitor of a SubMonitor.
Without it, the progress bar would never reach its maximum.
package com.vogella.tasks.ui.toolcontrols;
import java.util.Objects;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.ProgressProvider;
import org.eclipse.e4.ui.di.UISynchronize;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.ProgressBar;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
public class ProgressMonitorControl {
private final UISynchronize sync;
private ProgressBar progressBar;
// accessed only in the UI thread
private int runningJobs;
@Inject
public ProgressMonitorControl(UISynchronize sync) {
this.sync = Objects.requireNonNull(sync);
}
@PostConstruct
public void createControls(Composite parent) {
progressBar = new ProgressBar(parent, SWT.SMOOTH);
progressBar.setBounds(100, 10, 200, 20);
// every job scheduled afterwards gets its monitor from this provider
Job.getJobManager().setProgressProvider(new ProgressProvider() {
@Override
public IProgressMonitor createMonitor(Job job) {
return new ProgressBarMonitor();
}
});
// deregister the provider when the widget is disposed
progressBar.addDisposeListener(e -> Job.getJobManager().setProgressProvider(null));
}
// runs the update in the UI thread, unless the widget is already disposed
private void updateUi(Runnable update) {
sync.asyncExec(() -> {
if (!progressBar.isDisposed()) {
update.run();
}
});
}
private final class ProgressBarMonitor extends NullProgressMonitor {
// accessed only in the UI thread
private int totalWork;
private int reportedWork;
private boolean started;
private boolean finished;
@Override
public void beginTask(String name, int total) {
updateUi(() -> {
started = true;
totalWork = total;
if (runningJobs == 0) {
// no other job is running, reset the bar
progressBar.setSelection(0);
progressBar.setMaximum(total);
} else {
progressBar.setMaximum(progressBar.getMaximum() + total);
}
runningJobs++;
progressBar.setToolTipText("Running jobs: " + runningJobs);
});
}
@Override
public void worked(int work) {
updateUi(() -> report(work));
}
// called by the Jobs framework after the job has finished
@Override
public void done() {
updateUi(() -> {
if (!started || finished) {
return;
}
finished = true;
// report the work the job did not report itself, e.g.,
// the work assigned to the last child of a SubMonitor
report(totalWork - reportedWork);
runningJobs--;
progressBar.setToolTipText(
runningJobs == 0 ? "No background job running" : "Running jobs: " + runningJobs);
});
}
private void report(int work) {
if (work > 0) {
reportedWork += work;
progressBar.setSelection(progressBar.getSelection() + work);
}
}
}
}
Every job scheduled after the registration of the ProgressProvider automatically reports its progress to this progress bar.
Job job = Job.create("My Job", (ICoreRunnable) monitor -> {
// job implementation as before
});
job.schedule();
| A more advanced implementation could, for example, implement a progress monitoring OSGi service and report progress to the user interface via the event service. |
4. Handling job cancellation
Users can cancel a running job, for example, via the red cancel button in the Progress view. A job implementation is responsible for reacting to this cancellation request. Jobs which ignore the request keep running and result in a poor user experience.
The split() method of the SubMonitor checks whether the monitor has been canceled and throws an OperationCanceledException in this case.
This exception is automatically caught by the Job class, which then finishes the job with Status.CANCEL_STATUS.
A job which reports its progress via split() therefore handles cancellation without any additional code.
Job job = Job.create("Cancelable job", (ICoreRunnable) monitor -> {
SubMonitor subMonitor = SubMonitor.convert(monitor, tasks.size());
for (Task task : tasks) {
// split checks for cancellation and throws
// an OperationCanceledException if the job was canceled
workOnTask(task, subMonitor.split(1));
}
});
job.schedule();
Using split() is not only shorter than checking for cancellation manually, it also provides better performance, since the relatively expensive isCanceled() call is not performed on every invocation.
|
If you want to run clean-up code before the job ends, check isCanceled() yourself and create the child monitors via newChild(), which does not check for cancellation.
Throw an OperationCanceledException after the clean-up so that the job still finishes with the cancel status.
Job job = Job.create("Cancelable job", (ICoreRunnable) monitor -> {
SubMonitor subMonitor = SubMonitor.convert(monitor, tasks.size());
for (Task task : tasks) {
if (subMonitor.isCanceled()) {
// ... do the clean-up work here ...
throw new OperationCanceledException();
}
// newChild does not check for cancellation
workOnTask(task, subMonitor.newChild(1));
}
});
job.schedule();
5. Job families
Jobs can be grouped into families.
This allows you to handle a set of related jobs as one unit via the IJobManager, for example, to cancel all of them with a single call.
A job declares which families it belongs to by overriding the belongsTo() method.
A family can be identified by any object, for example, a shared constant.
As belongsTo() must be overridden, use a Job subclass instead of Job.create() for jobs which belong to a family.
public static final String MY_FAMILY = "myJobFamily";
Job job = new Job("Update task") {
@Override
protected IStatus run(IProgressMonitor monitor) {
SubMonitor subMonitor = SubMonitor.convert(monitor, 100);
// ... do the work
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return MY_FAMILY.equals(family);
}
};
job.schedule();
The IJobManager provides methods which operate on all jobs of a family.
IJobManager jobManager = Job.getJobManager();
// cancel all jobs of the family
jobManager.cancel(MY_FAMILY);
// find all jobs of the family
Job[] jobs = jobManager.find(MY_FAMILY);
// wait until all jobs of the family have finished,
// never call this from the UI thread
try {
jobManager.join(MY_FAMILY, null);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
join() blocks the calling thread until all jobs of the family have finished, therefore never call it from the UI thread.
|
6. Tutorial: Using Eclipse Jobs
Create a new Eclipse plug-in project "de.vogella.jobs.first" with a view and a button included in this view.
Create the following MySelectionAdapter class.
It schedules a job which simulates a long-running operation and opens a dialog in the UI thread once the work is done.
package de.vogella.jobs.first.parts;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.runtime.ICoreRunnable;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
public class MySelectionAdapter extends SelectionAdapter {
private final Shell shell;
public MySelectionAdapter(Shell shell) {
this.shell = shell;
}
@Override
public void widgetSelected(SelectionEvent e) {
Job job = Job.create("First Job", (ICoreRunnable) monitor -> {
doLongRunningOperation();
syncWithUi();
});
job.setUser(true);
job.schedule();
}
private void doLongRunningOperation() {
for (int i = 0; i < 10; i++) {
try {
// simulate a long-running operation
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
System.out.println("Doing something");
}
}
private void syncWithUi() {
// open the dialog in the UI thread,
// the shell might have been closed while the job was running
Display.getDefault().asyncExec(() -> {
if (!shell.isDisposed()) {
MessageDialog.openInformation(shell, "Job finished", "Your job has finished.");
}
});
}
}
Add an instance of MySelectionAdapter as a selection listener to your button.
Button button = new Button(parent, SWT.PUSH);
button.addSelectionListener(new MySelectionAdapter(shell));
To access the Shell in Eclipse 3.x you can use the getSite().getShell() method call.
In an Eclipse 4 application, you declare a field and let Eclipse inject the active Shell.
@Inject
Shell shell;
Start your application or the Eclipse workbench with your plug-in and press the button.
As the job is flagged as a user job via setUser(true), Eclipse reports its progress to the user, for example, in the Progress view.
After approximately ten seconds a dialog is opened, which tells you that the job has finished.
7. Using syncExec() and asyncExec()
If dependency injection is not available, for example, in Eclipse 3.x API based plug-ins, you cannot get the UISynchronize instance injected.
In this case you can use the Display class, which also provides the syncExec() and asyncExec() methods to update the user interface from another thread.
// update the user interface asynchronously
Display.getDefault().asyncExec(() -> {
// ... do any work that updates the screen ...
});
// update the user interface synchronously,
// syncExec blocks until the runnable has finished
Display.getDefault().syncExec(() -> {
// ... do any work that updates the screen ...
// remember to check if the widget still exists,
// the part might have been closed in the meantime
});
8. Learn more and get support
This tutorial continues on Eclipse RCP online training or Eclipse IDE extensions with lots of video material, additional exercises and much more content.
9. Eclipse Jobs resources
9.1. vogella Java example code
If you need more assistance we offer Online Training and Onsite training as well as consulting