summaryrefslogtreecommitdiff
path: root/src/threads/BackgroundJob.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/threads/BackgroundJob.vala')
-rw-r--r--src/threads/BackgroundJob.vala243
1 files changed, 243 insertions, 0 deletions
diff --git a/src/threads/BackgroundJob.vala b/src/threads/BackgroundJob.vala
new file mode 100644
index 0000000..178211e
--- /dev/null
+++ b/src/threads/BackgroundJob.vala
@@ -0,0 +1,243 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+// This callback is executed when an associated BackgroundJob completes. It is called from within
+// the Gtk event loop, *not* the background thread's context.
+public delegate void CompletionCallback(BackgroundJob job);
+
+// This callback is executed when an associated BackgroundJob has been cancelled (via its
+// Cancellable). Note that it's *possible* the BackgroundJob performed some or all of its work
+// prior to executing this delegate.
+public delegate void CancellationCallback(BackgroundJob job);
+
+// This callback is executed by the BackgroundJob when a unit of work is completed, but not the
+// entire job. It is called from within the Gtk event loop, *not* the background thread's
+// context.
+//
+// Note that there does not seem to be any guarantees of order in the Idle queue documentation,
+// and this it's possible (and, depending on assigned priorities, likely) that notifications could
+// arrive in different orders, and even after the CompletionCallback. Thus, no guarantee of
+// ordering is made here.
+//
+// NOTE: Would like Value to be nullable, but can't due to this bug:
+// https://bugzilla.gnome.org/show_bug.cgi?id=607098
+//
+// NOTE: There will be a memory leak using NotificationCallbacks due to this bug:
+// https://bugzilla.gnome.org/show_bug.cgi?id=571264
+//
+// NOTE: Because of these two bugs, using an abstract base class rather than Value. When both are
+// fixed (at least the second), may consider going back to Value.
+
+public abstract class NotificationObject {
+}
+
+public abstract class InterlockedNotificationObject : NotificationObject {
+ private Semaphore semaphore = new Semaphore();
+
+ // Only called by BackgroundJob; no need for users or subclasses to use
+ public void internal_wait_for_completion() {
+ semaphore.wait();
+ }
+
+ // Only called by BackgroundJob; no need for users or subclasses to use
+ public void internal_completed() {
+ semaphore.notify();
+ }
+}
+
+public delegate void NotificationCallback(BackgroundJob job, NotificationObject? user);
+
+// This abstract class represents a unit of work that can be executed within a background thread's
+// context. If specified, the job may be cancellable (which can be checked by execute() and the
+// worker thread prior to calling execute()). The BackgroundJob may also specify a
+// CompletionCallback and/or a CancellationCallback to be executed within Gtk's event loop.
+// A BackgroundJob may also emit NotificationCallbacks, all of which are also executed within
+// Gtk's event loop.
+//
+// The BackgroundJob may be constructed with a reference to its "owner". This is not used directly
+// by BackgroundJob or Worker, but merely exists to hold a reference to the Object that is receiving
+// the various callbacks from BackgroundJob. Without this, it's possible for the object creating
+// BackgroundJobs to be freed before all the callbacks have been received, or even during a callback,
+// which is an unstable situation.
+public abstract class BackgroundJob {
+ public enum JobPriority {
+ HIGHEST = 100,
+ HIGH = 75,
+ NORMAL = 50,
+ LOW = 25,
+ LOWEST = 0;
+
+ // Returns negative if this is higher, zero if equal, positive if this is lower
+ public int compare(JobPriority other) {
+ return (int) other - (int) this;
+ }
+
+ public static int compare_func(void *a, void *b) {
+ return (int) b - (int) a;
+ }
+ }
+
+ private class NotificationJob {
+ public unowned NotificationCallback callback;
+ public BackgroundJob background_job;
+ public NotificationObject? user;
+
+ public NotificationJob(NotificationCallback callback, BackgroundJob background_job,
+ NotificationObject? user) {
+ this.callback = callback;
+ this.background_job = background_job;
+ this.user = user;
+ }
+ }
+
+ private static Gee.ArrayList<NotificationJob> notify_queue = new Gee.ArrayList<NotificationJob>();
+
+ private Object owner;
+ private unowned CompletionCallback callback;
+ private Cancellable cancellable;
+ private unowned CancellationCallback cancellation;
+ private BackgroundJob self = null;
+ private AbstractSemaphore semaphore = null;
+
+ // The thinking here is that there is exactly one CompletionCallback per job, and the caller
+ // probably wants to know that to set off UI and other events in response. There are several
+ // (possibly hundreds or thousands) or notifications, and thus should arrive in a more
+ // controlled way (to avoid locking up the UI, for example). This has ramifications about
+ // the order in which completion and notifications arrive (see above note).
+ private int completion_priority = Priority.HIGH;
+ private int notification_priority = Priority.DEFAULT_IDLE;
+
+ public BackgroundJob(Object? owner = null, CompletionCallback? callback = null,
+ Cancellable? cancellable = null, CancellationCallback? cancellation = null,
+ AbstractSemaphore? completion_semaphore = null) {
+ this.owner = owner;
+ this.callback = callback;
+ this.cancellable = cancellable;
+ this.cancellation = cancellation;
+ this.semaphore = completion_semaphore;
+ }
+
+ public abstract void execute();
+
+ public virtual JobPriority get_priority() {
+ return JobPriority.NORMAL;
+ }
+
+ // For the CompareFunc delegate, according to JobPriority.
+ public static int priority_compare_func(BackgroundJob a, BackgroundJob b) {
+ return a.get_priority().compare(b.get_priority());
+ }
+
+ // For the Comparator delegate, according to JobPriority.
+ public static int64 priority_comparator(void *a, void *b) {
+ return priority_compare_func((BackgroundJob) a, (BackgroundJob) b);
+ }
+
+ // This method is not thread-safe. Best to set priority before the job is enqueued.
+ public void set_completion_priority(int priority) {
+ completion_priority = priority;
+ }
+
+ // This method is not thread-safe. Best to set priority before the job is enqueued.
+ public void set_notification_priority(int priority) {
+ notification_priority = priority;
+ }
+
+ // This method is thread-safe, but only waits if a completion semaphore has been set, otherwise
+ // exits immediately. Note that blocking for a semaphore does NOT spin the event loop, so a
+ // thread relying on it to continue should not use this.
+ public void wait_for_completion() {
+ if (semaphore != null)
+ semaphore.wait();
+ }
+
+ public Cancellable? get_cancellable() {
+ return cancellable;
+ }
+
+ public bool is_cancelled() {
+ return (cancellable != null) ? cancellable.is_cancelled() : false;
+ }
+
+ public void cancel() {
+ if (cancellable != null)
+ cancellable.cancel();
+ }
+
+ // This should only be called by Workers. Beware to all who fail to heed.
+ public void internal_notify_completion() {
+ if (semaphore != null)
+ semaphore.notify();
+
+ if (callback == null && cancellation == null)
+ return;
+
+ if (is_cancelled() && cancellation == null)
+ return;
+
+ // Because Idle doesn't maintain a ref count of the job, and it's going to be dropped by
+ // the worker thread soon, need to maintain a ref until the completion callback is made
+ self = this;
+
+ Idle.add_full(completion_priority, on_notify_completion);
+ }
+
+ private bool on_notify_completion() {
+ // it's still possible the caller cancelled this operation during or after the execute()
+ // method was called ... since the completion work can be costly for a job that was
+ // already cancelled, and the caller might've dropped all references to the job by now,
+ // only notify completion in this context if not cancelled
+ if (is_cancelled()) {
+ if (cancellation != null)
+ cancellation(this);
+ } else {
+ if (callback != null)
+ callback(this);
+ }
+
+ // drop the ref so this object can be freed ... must not touch "this" after this point
+ self = null;
+
+ return false;
+ }
+
+ // This call may be executed by the child class during execute() to inform of a unit of
+ // work being completed
+ protected void notify(NotificationCallback callback, NotificationObject? user) {
+ lock (notify_queue) {
+ notify_queue.add(new NotificationJob(callback, this, user));
+ }
+
+ Idle.add_full(notification_priority, on_notification_ready);
+
+ // If an interlocked notification, block until the main thread completes the notification
+ // callback
+ InterlockedNotificationObject? interlocked = user as InterlockedNotificationObject;
+ if (interlocked != null)
+ interlocked.internal_wait_for_completion();
+ }
+
+ private bool on_notification_ready() {
+ // this is called once for every notification added, so there should always be something
+ // waiting for us
+ NotificationJob? notification_job = null;
+ lock (notify_queue) {
+ if (notify_queue.size > 0)
+ notification_job = notify_queue.remove_at(0);
+ }
+ assert(notification_job != null);
+
+ notification_job.callback(notification_job.background_job, notification_job.user);
+
+ // Release the blocked thread waiting for this notification to complete
+ InterlockedNotificationObject? interlocked = notification_job.user as InterlockedNotificationObject;
+ if (interlocked != null)
+ interlocked.internal_completed();
+
+ return false;
+ }
+}
+