1 /* GIMP - The GNU Image Manipulation Program
2  * Copyright (C) 1995 Spencer Kimball and Peter Mattis
3  *
4  * gimpasync.c
5  * Copyright (C) 2018 Ell
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  */
20 
21 #include "config.h"
22 
23 #include <gdk-pixbuf/gdk-pixbuf.h>
24 #include <gegl.h>
25 
26 #include "core-types.h"
27 
28 #include "gimpasync.h"
29 #include "gimpcancelable.h"
30 #include "gimpmarshal.h"
31 #include "gimpwaitable.h"
32 
33 
34 /* GimpAsync represents an asynchronous task.  Both the public and the
35  * protected interfaces are intentionally minimal at this point, to keep things
36  * simple.  They may be extended in the future as needed.
37  *
38  * GimpAsync implements the GimpWaitable and GimpCancelable interfaces.
39  *
40  * Upon creation, a GimpAsync object is in the "running" state.  Once the task
41  * is complete (and before the object's destruction), it should be transitioned
42  * to the "stopped" state, using either 'gimp_async_finish()' or
43  * 'gimp_async_abort()'.
44  *
45  * Similarly, upon creation, a GimpAsync object is said to be "unsynced".  It
46  * becomes synced once the execution of any of the completion callbacks added
47  * through 'gimp_async_add_callback()' had started, or after a successful call
48  * to one of the 'gimp_waitable_wait()' family of functions.
49  *
50  * Note that certain GimpAsync functions may only be called during a certain
51  * state, on a certain thread, or depending on whether or not the object is
52  * synced, as detailed for each function.  When referring to threads, the "main
53  * thread" is the thread running the main loop, or any thread whose execution
54  * is synchronized with the main thread, and the "async thread" is the thread
55  * calling 'gimp_async_finish()' or 'gimp_async_abort()' (which may also be the
56  * main thread), or any thread whose execution is synchronized with the async
57  * thread.
58  */
59 
60 
61 /* #define TIME_ASYNC_OPS */
62 
63 
64 enum
65 {
66   WAITING,
67   LAST_SIGNAL
68 };
69 
70 
71 typedef struct _GimpAsyncCallbackInfo GimpAsyncCallbackInfo;
72 
73 
74 struct _GimpAsyncCallbackInfo
75 {
76   GimpAsync         *async;
77   GimpAsyncCallback  callback;
78   gpointer           data;
79   gpointer           gobject;
80 };
81 
82 struct _GimpAsyncPrivate
83 {
84   GMutex         mutex;
85   GCond          cond;
86 
87   GQueue         callbacks;
88 
89   gpointer       result;
90   GDestroyNotify result_destroy_func;
91 
92   guint          idle_id;
93 
94   gboolean       stopped;
95   gboolean       finished;
96   gboolean       synced;
97   gboolean       canceled;
98 
99 #ifdef TIME_ASYNC_OPS
100   guint64        start_time;
101 #endif
102 };
103 
104 
105 /*  local function prototypes  */
106 
107 static void       gimp_async_waitable_iface_init   (GimpWaitableInterface   *iface);
108 
109 static void       gimp_async_cancelable_iface_init (GimpCancelableInterface *iface);
110 
111 static void       gimp_async_finalize              (GObject                 *object);
112 
113 static void       gimp_async_wait                  (GimpWaitable            *waitable);
114 static gboolean   gimp_async_try_wait              (GimpWaitable            *waitable);
115 static gboolean   gimp_async_wait_until            (GimpWaitable            *waitable,
116                                                     gint64                   end_time);
117 
118 static void       gimp_async_cancel                (GimpCancelable          *cancelable);
119 
120 static gboolean   gimp_async_idle                  (GimpAsync               *async);
121 
122 static void       gimp_async_callback_weak_notify  (GimpAsyncCallbackInfo   *callback_info,
123                                                     GObject                 *gobject);
124 
125 static void       gimp_async_stop                  (GimpAsync               *async);
126 static void       gimp_async_run_callbacks         (GimpAsync               *async);
127 
128 
129 G_DEFINE_TYPE_WITH_CODE (GimpAsync, gimp_async, G_TYPE_OBJECT,
130                          G_ADD_PRIVATE (GimpAsync)
131                          G_IMPLEMENT_INTERFACE (GIMP_TYPE_WAITABLE,
132                                                 gimp_async_waitable_iface_init)
133                          G_IMPLEMENT_INTERFACE (GIMP_TYPE_CANCELABLE,
134                                                 gimp_async_cancelable_iface_init))
135 
136 #define parent_class gimp_async_parent_class
137 
138 static guint async_signals[LAST_SIGNAL] = { 0 };
139 
140 
141 /*  local variables  */
142 
143 static volatile gint gimp_async_n_running = 0;
144 
145 
146 /*  private functions  */
147 
148 
149 static void
gimp_async_class_init(GimpAsyncClass * klass)150 gimp_async_class_init (GimpAsyncClass *klass)
151 {
152   GObjectClass *object_class = G_OBJECT_CLASS (klass);
153 
154   async_signals[WAITING] =
155     g_signal_new ("waiting",
156                   G_TYPE_FROM_CLASS (klass),
157                   G_SIGNAL_RUN_FIRST,
158                   G_STRUCT_OFFSET (GimpAsyncClass, waiting),
159                   NULL, NULL,
160                   gimp_marshal_VOID__VOID,
161                   G_TYPE_NONE, 0);
162 
163   object_class->finalize = gimp_async_finalize;
164 }
165 
166 static void
gimp_async_waitable_iface_init(GimpWaitableInterface * iface)167 gimp_async_waitable_iface_init (GimpWaitableInterface *iface)
168 {
169   iface->wait       = gimp_async_wait;
170   iface->try_wait   = gimp_async_try_wait;
171   iface->wait_until = gimp_async_wait_until;
172 }
173 
174 static void
gimp_async_cancelable_iface_init(GimpCancelableInterface * iface)175 gimp_async_cancelable_iface_init (GimpCancelableInterface *iface)
176 {
177   iface->cancel = gimp_async_cancel;
178 }
179 
180 static void
gimp_async_init(GimpAsync * async)181 gimp_async_init (GimpAsync *async)
182 {
183   async->priv = gimp_async_get_instance_private (async);
184 
185   g_mutex_init (&async->priv->mutex);
186   g_cond_init  (&async->priv->cond);
187 
188   g_queue_init (&async->priv->callbacks);
189 
190   g_atomic_int_inc (&gimp_async_n_running);
191 
192 #ifdef TIME_ASYNC_OPS
193   async->priv->start_time = g_get_monotonic_time ();
194 #endif
195 }
196 
197 static void
gimp_async_finalize(GObject * object)198 gimp_async_finalize (GObject *object)
199 {
200   GimpAsync *async = GIMP_ASYNC (object);
201 
202   g_warn_if_fail (async->priv->stopped);
203   g_warn_if_fail (async->priv->idle_id == 0);
204   g_warn_if_fail (g_queue_is_empty (&async->priv->callbacks));
205 
206   if (async->priv->finished &&
207       async->priv->result   &&
208       async->priv->result_destroy_func)
209     {
210       async->priv->result_destroy_func (async->priv->result);
211 
212       async->priv->result = NULL;
213     }
214 
215   g_cond_clear  (&async->priv->cond);
216   g_mutex_clear (&async->priv->mutex);
217 
218   G_OBJECT_CLASS (parent_class)->finalize (object);
219 }
220 
221 /* waits for 'waitable' to transition to the "stopped" state.  if 'waitable' is
222  * already stopped, returns immediately.
223  *
224  * after the call, all callbacks previously added through
225  * 'gimp_async_add_callback()' are guaranteed to have been called.
226  *
227  * may only be called on the main thread.
228  */
229 static void
gimp_async_wait(GimpWaitable * waitable)230 gimp_async_wait (GimpWaitable *waitable)
231 {
232   GimpAsync *async = GIMP_ASYNC (waitable);
233 
234   g_mutex_lock (&async->priv->mutex);
235 
236   if (! async->priv->stopped)
237     {
238       g_signal_emit (async, async_signals[WAITING], 0);
239 
240       while (! async->priv->stopped)
241         g_cond_wait (&async->priv->cond, &async->priv->mutex);
242     }
243 
244   g_mutex_unlock (&async->priv->mutex);
245 
246   gimp_async_run_callbacks (async);
247 }
248 
249 /* same as 'gimp_async_wait()', but returns immediately if 'waitable' is not in
250  * the "stopped" state.
251  *
252  * returns TRUE if 'waitable' has transitioned to the "stopped" state, or FALSE
253  * otherwise.
254  */
255 static gboolean
gimp_async_try_wait(GimpWaitable * waitable)256 gimp_async_try_wait (GimpWaitable *waitable)
257 {
258   GimpAsync *async = GIMP_ASYNC (waitable);
259 
260   g_mutex_lock (&async->priv->mutex);
261 
262   if (! async->priv->stopped)
263     {
264       g_mutex_unlock (&async->priv->mutex);
265 
266       return FALSE;
267     }
268 
269   g_mutex_unlock (&async->priv->mutex);
270 
271   gimp_async_run_callbacks (async);
272 
273   return TRUE;
274 }
275 
276 /* same as 'gimp_async_wait()', taking an additional 'end_time' parameter,
277  * specifying the maximal monotonic time until which to wait for 'waitable' to
278  * stop.
279  *
280  * returns TRUE if 'waitable' has transitioned to the "stopped" state, or FALSE
281  * if the wait was interrupted before the transition.
282  */
283 static gboolean
gimp_async_wait_until(GimpWaitable * waitable,gint64 end_time)284 gimp_async_wait_until (GimpWaitable *waitable,
285                        gint64        end_time)
286 {
287   GimpAsync *async = GIMP_ASYNC (waitable);
288 
289   g_mutex_lock (&async->priv->mutex);
290 
291   if (! async->priv->stopped)
292     {
293       g_signal_emit (async, async_signals[WAITING], 0);
294 
295       while (! async->priv->stopped)
296         {
297           if (! g_cond_wait_until (&async->priv->cond, &async->priv->mutex,
298                                    end_time))
299             {
300               g_mutex_unlock (&async->priv->mutex);
301 
302               return FALSE;
303             }
304         }
305     }
306 
307   g_mutex_unlock (&async->priv->mutex);
308 
309   gimp_async_run_callbacks (async);
310 
311   return TRUE;
312 }
313 
314 /* requests the cancellation of the task managed by 'cancelable'.
315  *
316  * note that 'gimp_async_cancel()' doesn't directly cause 'cancelable' to be
317  * stopped, nor synced.  furthermore, 'cancelable' may still complete
318  * successfully even when cancellation has been requested.
319  *
320  * may only be called on the main thread.
321  */
322 static void
gimp_async_cancel(GimpCancelable * cancelable)323 gimp_async_cancel (GimpCancelable *cancelable)
324 {
325   GimpAsync *async = GIMP_ASYNC (cancelable);
326 
327   async->priv->canceled = TRUE;
328 }
329 
330 static gboolean
gimp_async_idle(GimpAsync * async)331 gimp_async_idle (GimpAsync *async)
332 {
333   gimp_waitable_wait (GIMP_WAITABLE (async));
334 
335   return G_SOURCE_REMOVE;
336 }
337 
338 static void
gimp_async_callback_weak_notify(GimpAsyncCallbackInfo * callback_info,GObject * gobject)339 gimp_async_callback_weak_notify (GimpAsyncCallbackInfo *callback_info,
340                                  GObject               *gobject)
341 {
342   GimpAsync *async       = callback_info->async;
343   gboolean   unref_async = FALSE;
344 
345   g_mutex_lock (&async->priv->mutex);
346 
347   g_queue_remove (&async->priv->callbacks, callback_info);
348 
349   g_slice_free (GimpAsyncCallbackInfo, callback_info);
350 
351   if (g_queue_is_empty (&async->priv->callbacks) && async->priv->idle_id)
352     {
353       g_source_remove (async->priv->idle_id);
354       async->priv->idle_id = 0;
355 
356       unref_async = TRUE;
357     }
358 
359   g_mutex_unlock (&async->priv->mutex);
360 
361   if (unref_async)
362     g_object_unref (async);
363 }
364 
365 static void
gimp_async_stop(GimpAsync * async)366 gimp_async_stop (GimpAsync *async)
367 {
368 #ifdef TIME_ASYNC_OPS
369   {
370     guint64 time = g_get_monotonic_time ();
371 
372     g_printerr ("Asynchronous operation took %g seconds%s\n",
373                 (time - async->priv->start_time) / 1000000.0,
374                 async->priv->finished ? "" : " (aborted)");
375   }
376 #endif
377 
378   g_atomic_int_dec_and_test (&gimp_async_n_running);
379 
380   if (! g_queue_is_empty (&async->priv->callbacks))
381     {
382       g_object_ref (async);
383 
384       async->priv->idle_id = g_idle_add_full (G_PRIORITY_DEFAULT,
385                                               (GSourceFunc) gimp_async_idle,
386                                               async, NULL);
387     }
388 
389   async->priv->stopped = TRUE;
390 
391   g_cond_broadcast (&async->priv->cond);
392 }
393 
394 static void
gimp_async_run_callbacks(GimpAsync * async)395 gimp_async_run_callbacks (GimpAsync *async)
396 {
397   GimpAsyncCallbackInfo *callback_info;
398   gboolean               unref_async = FALSE;
399 
400   if (async->priv->idle_id)
401     {
402       g_source_remove (async->priv->idle_id);
403       async->priv->idle_id = 0;
404 
405       unref_async = TRUE;
406     }
407 
408   async->priv->synced = TRUE;
409 
410   while ((callback_info = g_queue_pop_head (&async->priv->callbacks)))
411     {
412       if (callback_info->gobject)
413         {
414           g_object_ref (callback_info->gobject);
415 
416           g_object_weak_unref (callback_info->gobject,
417                                (GWeakNotify) gimp_async_callback_weak_notify,
418                                callback_info);
419         }
420 
421       callback_info->callback (async, callback_info->data);
422 
423       if (callback_info->gobject)
424         g_object_unref (callback_info->gobject);
425 
426       g_slice_free (GimpAsyncCallbackInfo, callback_info);
427     }
428 
429   if (unref_async)
430     g_object_unref (async);
431 }
432 
433 
434 /*  public functions  */
435 
436 
437 /* creates a new GimpAsync object, initially unsynced and placed in the
438  * "running" state.
439  */
440 GimpAsync *
gimp_async_new(void)441 gimp_async_new (void)
442 {
443   return g_object_new (GIMP_TYPE_ASYNC,
444                        NULL);
445 }
446 
447 /* checks if 'async' is synced.
448  *
449  * may only be called on the main thread.
450  */
451 gboolean
gimp_async_is_synced(GimpAsync * async)452 gimp_async_is_synced (GimpAsync *async)
453 {
454   g_return_val_if_fail (GIMP_IS_ASYNC (async), FALSE);
455 
456   return async->priv->synced;
457 }
458 
459 /* registers a callback to be called when 'async' transitions to the "stopped"
460  * state.  if 'async' is already stopped, the callback may be called directly.
461  *
462  * callbacks are called in the order in which they were added.  'async' is
463  * guaranteed to be kept alive, even without an external reference, between the
464  * point where it was stopped, and until all callbacks added while 'async' was
465  * externally referenced have been called.
466  *
467  * the callback is guaranteed to be called on the main thread.
468  *
469  * may only be called on the main thread.
470  */
471 void
gimp_async_add_callback(GimpAsync * async,GimpAsyncCallback callback,gpointer data)472 gimp_async_add_callback (GimpAsync         *async,
473                          GimpAsyncCallback  callback,
474                          gpointer           data)
475 {
476   GimpAsyncCallbackInfo *callback_info;
477 
478   g_return_if_fail (GIMP_IS_ASYNC (async));
479   g_return_if_fail (callback != NULL);
480 
481   g_mutex_lock (&async->priv->mutex);
482 
483   if (async->priv->stopped && g_queue_is_empty (&async->priv->callbacks))
484     {
485       async->priv->synced = TRUE;
486 
487       g_mutex_unlock (&async->priv->mutex);
488 
489       callback (async, data);
490 
491       return;
492     }
493 
494   callback_info           = g_slice_new0 (GimpAsyncCallbackInfo);
495   callback_info->async    = async;
496   callback_info->callback = callback;
497   callback_info->data     = data;
498 
499   g_queue_push_tail (&async->priv->callbacks, callback_info);
500 
501   g_mutex_unlock (&async->priv->mutex);
502 }
503 
504 /* same as 'gimp_async_add_callback()', however, takes an additional 'gobject'
505  * argument, which should be a GObject.
506  *
507  * 'gobject' is guaranteed to remain alive for the duration of the callback.
508  *
509  * When 'gobject' is destroyed, the callback is automatically removed.
510  */
511 void
gimp_async_add_callback_for_object(GimpAsync * async,GimpAsyncCallback callback,gpointer data,gpointer gobject)512 gimp_async_add_callback_for_object (GimpAsync         *async,
513                                     GimpAsyncCallback  callback,
514                                     gpointer           data,
515                                     gpointer           gobject)
516 {
517   GimpAsyncCallbackInfo *callback_info;
518 
519   g_return_if_fail (GIMP_IS_ASYNC (async));
520   g_return_if_fail (callback != NULL);
521   g_return_if_fail (G_IS_OBJECT (gobject));
522 
523   g_mutex_lock (&async->priv->mutex);
524 
525   if (async->priv->stopped && g_queue_is_empty (&async->priv->callbacks))
526     {
527       async->priv->synced = TRUE;
528 
529       g_mutex_unlock (&async->priv->mutex);
530 
531       g_object_ref (gobject);
532 
533       callback (async, data);
534 
535       g_object_unref (gobject);
536 
537       return;
538     }
539 
540   callback_info           = g_slice_new0 (GimpAsyncCallbackInfo);
541   callback_info->async    = async;
542   callback_info->callback = callback;
543   callback_info->data     = data;
544   callback_info->gobject  = gobject;
545 
546   g_queue_push_tail (&async->priv->callbacks, callback_info);
547 
548   g_object_weak_ref (gobject,
549                      (GWeakNotify) gimp_async_callback_weak_notify,
550                      callback_info);
551 
552   g_mutex_unlock (&async->priv->mutex);
553 }
554 
555 /* removes all callbacks previously registered through
556  * 'gimp_async_add_callback()', matching 'callback' and 'data', which hasn't
557  * been called yet.
558  *
559  * may only be called on the main thread.
560  */
561 void
gimp_async_remove_callback(GimpAsync * async,GimpAsyncCallback callback,gpointer data)562 gimp_async_remove_callback (GimpAsync         *async,
563                             GimpAsyncCallback  callback,
564                             gpointer           data)
565 {
566   GList    *iter;
567   gboolean  unref_async = FALSE;
568 
569   g_return_if_fail (GIMP_IS_ASYNC (async));
570   g_return_if_fail (callback != NULL);
571 
572   g_mutex_lock (&async->priv->mutex);
573 
574   iter = g_queue_peek_head_link (&async->priv->callbacks);
575 
576   while (iter)
577     {
578       GimpAsyncCallbackInfo *callback_info = iter->data;
579       GList                 *next          = g_list_next (iter);
580 
581       if (callback_info->callback == callback &&
582           callback_info->data     == data)
583         {
584           if (callback_info->gobject)
585             {
586               g_object_weak_unref (
587                 callback_info->gobject,
588                 (GWeakNotify) gimp_async_callback_weak_notify,
589                 callback_info);
590             }
591 
592           g_queue_delete_link (&async->priv->callbacks, iter);
593 
594           g_slice_free (GimpAsyncCallbackInfo, callback_info);
595         }
596 
597       iter = next;
598     }
599 
600   if (g_queue_is_empty (&async->priv->callbacks) && async->priv->idle_id)
601     {
602       g_source_remove (async->priv->idle_id);
603       async->priv->idle_id = 0;
604 
605       unref_async = TRUE;
606     }
607 
608   g_mutex_unlock (&async->priv->mutex);
609 
610   if (unref_async)
611     g_object_unref (async);
612 }
613 
614 /* checks if 'async' is in the "stopped" state.
615  *
616  * may only be called on the async thread.
617  */
618 gboolean
gimp_async_is_stopped(GimpAsync * async)619 gimp_async_is_stopped (GimpAsync *async)
620 {
621   g_return_val_if_fail (GIMP_IS_ASYNC (async), FALSE);
622 
623   return async->priv->stopped;
624 }
625 
626 /* transitions 'async' to the "stopped" state, indicating that the task
627  * completed normally, possibly providing a result.
628  *
629  * 'async' shall be in the "running" state.
630  *
631  * may only be called on the async thread.
632  */
633 void
gimp_async_finish(GimpAsync * async,gpointer result)634 gimp_async_finish (GimpAsync *async,
635                    gpointer   result)
636 {
637   gimp_async_finish_full (async, result, NULL);
638 }
639 
640 /* same as 'gimp_async_finish()', taking an additional GDestroyNotify function,
641  * used for freeing the result when 'async' is destroyed.
642  */
643 void
gimp_async_finish_full(GimpAsync * async,gpointer result,GDestroyNotify result_destroy_func)644 gimp_async_finish_full (GimpAsync      *async,
645                         gpointer        result,
646                         GDestroyNotify  result_destroy_func)
647 {
648   g_return_if_fail (GIMP_IS_ASYNC (async));
649   g_return_if_fail (! async->priv->stopped);
650 
651   g_mutex_lock (&async->priv->mutex);
652 
653   async->priv->finished            = TRUE;
654   async->priv->result              = result;
655   async->priv->result_destroy_func = result_destroy_func;
656 
657   gimp_async_stop (async);
658 
659   g_mutex_unlock (&async->priv->mutex);
660 }
661 
662 /* checks if 'async' completed normally, using 'gimp_async_finish()' (in
663  * contrast to 'gimp_async_abort()').
664  *
665  * 'async' shall be in the "stopped" state.
666  *
667  * may only be called on the async thread, or on the main thread when 'async'
668  * is synced.
669  */
670 gboolean
gimp_async_is_finished(GimpAsync * async)671 gimp_async_is_finished (GimpAsync *async)
672 {
673   g_return_val_if_fail (GIMP_IS_ASYNC (async), FALSE);
674   g_return_val_if_fail (async->priv->stopped, FALSE);
675 
676   return async->priv->finished;
677 }
678 
679 /* returns the result of 'async', as passed to 'gimp_async_finish()'.
680  *
681  * 'async' shall be in the "stopped" state, and should have completed normally.
682  *
683  * may only be called on the async thread, or on the main thread when 'async'
684  * is synced.
685  */
686 gpointer
gimp_async_get_result(GimpAsync * async)687 gimp_async_get_result (GimpAsync *async)
688 {
689   g_return_val_if_fail (GIMP_IS_ASYNC (async), NULL);
690   g_return_val_if_fail (async->priv->stopped, NULL);
691   g_return_val_if_fail (async->priv->finished, NULL);
692 
693   return async->priv->result;
694 }
695 
696 /* transitions 'async' to the "stopped" state, indicating that the task
697  * was stopped before completion (normally, in response to a
698  * 'gimp_cancelable_cancel()' call).
699  *
700  * 'async' shall be in the "running" state.
701  *
702  * may only be called on the async thread.
703  */
704 void
gimp_async_abort(GimpAsync * async)705 gimp_async_abort (GimpAsync *async)
706 {
707   g_return_if_fail (GIMP_IS_ASYNC (async));
708   g_return_if_fail (! async->priv->stopped);
709 
710   g_mutex_lock (&async->priv->mutex);
711 
712   gimp_async_stop (async);
713 
714   g_mutex_unlock (&async->priv->mutex);
715 }
716 
717 /* checks if cancellation of 'async' has been requested.
718  *
719  * note that a return value of TRUE only indicates that
720  * 'gimp_cancelable_cancel()' has been called for 'async', and not that 'async'
721  * is stopped, or, if it is stopped, that it was aborted.
722  */
723 gboolean
gimp_async_is_canceled(GimpAsync * async)724 gimp_async_is_canceled (GimpAsync *async)
725 {
726   g_return_val_if_fail (GIMP_IS_ASYNC (async), FALSE);
727 
728   return async->priv->canceled;
729 }
730 
731 /* a convenience function, canceling 'async' and waiting for it to stop.
732  *
733  * may only be called on the main thread.
734  */
735 void
gimp_async_cancel_and_wait(GimpAsync * async)736 gimp_async_cancel_and_wait (GimpAsync *async)
737 {
738   g_return_if_fail (GIMP_IS_ASYNC (async));
739 
740   gimp_cancelable_cancel (GIMP_CANCELABLE (async));
741   gimp_waitable_wait (GIMP_WAITABLE (async));
742 }
743 
744 
745 /*  public functions (stats)  */
746 
747 
748 gint
gimp_async_get_n_running(void)749 gimp_async_get_n_running (void)
750 {
751   return gimp_async_n_running;
752 }
753