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
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 also called the UI thread, but this is a misnomer as it handles all events not only the 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 also provides the Eclipse Jobs framework which allows you to run operations in the background and providing feedback of 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 creation and scheduling of a
Job
is demonstrated in the following code snippet.
// get UISynchronize injected as field
@Inject UISynchronize sync;
// more code
Job job = Job.create("Update table", (ICoreRunnable) monitor -> {
// do something long running
// ...
// If you want to update the UI
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 in
which
order
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 simply want to give the user the feedback that something is running without using threads.
The easiest
way to provide feedback is to change
the
cursor via the
BusyIndicator.showWhile()
method call.
// Show a busy indicator while the runnable is executed
BusyIndicator.showWhile(display, runnable);
If this code is executed, the cursor will change to a
busy
indicator
until the
Runnable
is done.
3. Reporting Progress
3.1. IProgressMonitor and the SubMonitor
An instance of an
IProgressMonitor
is passed to the run method of a
Job
and can be used to monitor the
progress of the job. When methods inside the job need to report
feedback on the
progress, a child of the
Submonitor
is passed rather than passing the actual SubMonitor. It is good practice to convert the
IProgressMonitor
always
to a
SubMonitor
before using it. This allows to use a consistent API for process reporting of child processes and the main process.
A
SubMonitor
can be created similar to this:
SubMonitor subMonitor = SubMonitor.convert(monitor, 3);
final List<Task> tasks = taskService.getTasks();
Job job = new Job("My Job") {
@Override
protected IStatus run(IProgressMonitor monitor) {
// convert to SubMonitor and set total number of work units
SubMonitor subMonitor = SubMonitor.convert(monitor, tasks.size());
for (Task task : tasks) {
try {
// sleep a second
TimeUnit.SECONDS.sleep(1);
// set the name of the current work
subMonitor.setTaskName("I'm working on Task " + task.getSummary());
// workOnTask is a method in this class which does some work
// pass a new child with the totalWork of 1 to the mehtod
workOnTask(task, subMonitor.split(1));
} catch (InterruptedException e) {
return Status.CANCEL_STATUS;
}
}
return Status.OK_STATUS;
}
};
job.schedule();
When using a
SubMonitor
it is not necessary to call the
beginTask()
method or the
done()
method, since this is done implicitly by the
SubMonitor
implementation.
3.2. Taking conditions during progress into account
In some cases the amount of work depends on conditions, which should also be properly reported.
This can be done by using the
setWorkRemaining()
method of the
SubMonitor
.
protected IStatus run(IProgressMonitor monitor) {
// convert to SubMonitor and set 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 only 70 % of the work remains
subMonitor.setWorkRemaining(70);
// do the rest of the work
workOnTask(task, subMonitor.split(70));
return Status.OK_STATUS;
}
In case the code in the
taskNeedsPreconfiguration()
if block is run, the
setWorkRemaining()
method actually does nothing. Only in case the code of the if block
is skipped it ensures that the process monitoring
is done properly.
Another use case for using
setWorkRemaining()
is when the actual work is determined later. See
workOnTask
method.
protected IStatus run(IProgressMonitor monitor) {
// convert to SubMonitor and set 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 only 70 % of the work remains
subMonitor.setWorkRemaining(70);
// do the rest of the work
workOnTask(task, subMonitor.split(70));
return Status.OK_STATUS;
}
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("Work on Task " + task.getSummary());
SubMonitor loopRelatedTasksMonitor = subMonitor.split(80);
// get related tasks from a service, which should also be preconfigured
List<Task> relatedTasks = taskService.findRelatedTasks(task);
// setWorkRemaining can also be applied, if the actual work is determined later
loopRelatedTasksMonitor.setWorkRemaining(relatedTasks.size());
for (Task relatedtask : relatedTasks) {
preConfigureTask(relatedtask, loopRelatedTasksMonitor.split(1));
}
// ... do work on the actual task with the remaining 20 %
doWorkOnActualTask(task, subMonitor.split(20));
}
For the loop in the
workOnTask
a new child
SubMonitor
, which is supposed to do 80 % of the work, is created. Later on the
actual remaining work for this
SubMonitor
is set by the
setWorkRemaining()
.
3.3. Reporting progress in Eclipse RCP applications
In Eclipse applications you can report progress by implementing the
IProgressMonitor
interface.
You can, for example, add a tool control
to a toolbar in your
application model. This tool control
can
implement the
IProgressMonitor
interface to show the progress.
This is demonstrated in the following example.
package com.vogella.tasks.ui.toolcontrols;
import java.util.Objects;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Inject;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
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;
public class ProgressMonitorControl {
private final UISynchronize sync;
private ProgressBar progressBar;
private GobalProgressMonitor monitor;
@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);
Job.getJobManager().setProgressProvider(new ProgressProvider() {
@Override
public IProgressMonitor createMonitor(Job job) {
StatusUpdateProgressMonitor statusUpdateProgressMonitor = new StatusUpdateProgressMonitor();
return statusUpdateProgressMonitor.addJob(job);
}
});
private final class StatusUpdateProgressMonitor extends NullProgressMonitor {
// thread-Safe via thread confinement of the UI-Thread
// (means access only via UI-Thread)
private long runningTasks = 0L;
@Override
public void beginTask(final String name, final int totalWork) {
sync.syncExec(new Runnable() {
@Override
public void run() {
if( runningTasks <= 0 ) {
// --- no task is running at the moment ---
progressBar.setSelection(0);
progressBar.setMaximum(totalWork);
} else {
// --- other tasks are running ---
progressBar.setMaximum(progressBar.getMaximum() + totalWork);
}
runningTasks++;
progressBar.setToolTipText("Currently running: " + runningTasks +
"\nLast task: " + name);
}
});
}
@Override
public void worked(final int work) {
sync.syncExec(new Runnable() {
@Override
public void run() {
progressBar.setSelection(progressBar.getSelection() + work);
}
});
}
public IProgressMonitor addJob(Job job){
if( job != null ){
job.addJobChangeListener(new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent event) {
sync.syncExec(new Runnable() {
@Override
public void run() {
runningTasks--;
if (runningTasks > 0 ){
// --- some tasks are still running ---
progressBar.setToolTipText("Currently running: " + runningTasks);
} else {
// --- all tasks are done (a reset of selection could also be done) ---
progressBar.setToolTipText("No background progress running.");
}
}
});
// clean-up
event.getJob().removeJobChangeListener(this);
}
});
}
return this;
}
}
}
This new element can be accessed via the model service and used as an
IProgressMonitor
for the job.
Job job = new Job("My Job") {
// code 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. Reporting Progress in Eclipse 3.x
To activate progress reporting in the status line in Eclipse 3.x you
have to activate
progress reporting in
preWindowOpen()
method of the
WorkbenchWindowAdvisor
.
5. Handle Job cancellation
In Eclipse many user complain about jobs, which are not properly
canceled when pressing the red square cancel button in the progess
view. Some
Job
implementations simply just don’t care and do not react on the
cancellation, which is triggered by the user.
This of course concludes in a really poor user experience, which should definitely be avoided.
In order to realize that a user has canceled a Job the
IProgressMonitor
, which is passed to the
run
method of a Job, can be asked whether the Job has been canceled. This
can be done by calling the
isCanceled
on an
IProgressMonitor
.
This check for cancellation should be done in a frequent manner, so that the user does not have to wait too long until the job will really be canceled.
5.1. Handle Job cancellation before Eclipse Neon (4.6)
Job job = Job.create("CancelAbleJob", monitor -> {
SubMonitor subMonitor = SubMonitor.convert(monitor, todos.size());
for (Todo todo : todos) {
if(subMonitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
processTodo(subMonitor.newChild(1), todo);
}
return Status.OK_STATUS;
});
job.schedule();
5.2. Handling Job cancellation since Eclipse Neon (4.6)
Job job = Job.create("CancelAbleJob", monitor -> {
SubMonitor subMonitor = SubMonitor.convert(monitor, todos.size());
for (Todo todo : todos) {
processTodo(subMonitor.split(1), todo);
}
// no return value needed when using an ICoreRunnable (since Neon)
});
job.schedule();
The new
split
method can handle the cancellation check automatically and throws an
OperationCanceledException
in case the
IProgressMonitor
has been canceled.
The OperationCanceledException
is automatically caught by a Job
, which will just set the Status.CANCEL_STATUS
then.
Using the new Submonitor’s
split
is not only shorter than the former approach, but also provides
better performance since the expensive
isCanceled
call is only done in case it is really necessary.
|
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.
package de.vogella.jobs.first.parts;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
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 = new Job("First Job") {
@Override
protected IStatus run(IProgressMonitor monitor) {
doLongThing();
syncWithUi();
// use this to open a Shell in the UI thread
return Status.OK_STATUS;
}
};
job.setUser(true);
job.schedule();
}
private void doLongThing() {
for (int i = 0; i < 10; i++) {
try {
// We simulate a long running operation here
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Doing something");
}
}
private void syncWithUi() {
Display.getDefault().asyncExec(new Runnable() {
public void run() {
MessageDialog.openInformation(shell, "Your Popup ",
"Your job has finished.");
}
});
}
}
Add an instance of
MySelectionAdapter
as
SelectionListener
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 Eclipse 4 you declare a field and let Eclipse inject
the
Shell
.
@Inject Shell shell
Start your application or the Eclipse workbench with your plug-in and press the Button. A dialog is opened.
7. Using syncExec() and asyncExec()
In Eclipse 3.x API based plug-ins you cannot use dependency injection
to get the
UISynchronize
instance injected.
In this case you can use the
Display
class which provides the
syncExec()
and
asyncExec()
methods to update the user interface from another
thread.
// Update the user interface asynchronously
Display.getDefault().asyncExec(new Runnable() {
public void run() {
// ... do any work that updates the screen ...
}
});
// Update the user interface synchronously
Display.getDefault().syncExec(new Runnable() {
public void run() {
// do any work that updates the screen ...
// remember to check if the widget
// still exists
// might happen if the part was closed
}
});
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