Disposing of a GTask when GCancellable cancelled

Hello,

I am migrating some multi threaded code to use GTask but I’m getting stuck in my implementation. I have read through the cancellation process of GTask but my understanding is still a little muddy.

Here’s what I’m looking to have happen:

  • An GObject is created after the user configures the parameters of its operation from the UI and clicks a button.
  • The object creates a GTask during its construction and calls g_task_run_in_thread () to do the operation asynchronously.
  • The user can cancel the operation by emitting a signal from the UI’s default main context (via a button), calling g_cancellable_cancel(). If this happens, I am looking to have the GTask return (i.e stop any further operations).
    -Once the GTask has been stopped, I want the object to be disposed.

What I have experienced this far using g_task_return_on_cancel () with the flag set to TRUE does exactly what the API description says (return control to calling function and let the task finish out it’s operation), but it’s not what I want. Setting this flag to FALSE triggers the GAsyncReadyCallback but the operations still continue in the background.

I want both the callback to be triggered and the GTask returned on cancel. Does this mean that in each operation would I need to check the cancellable for its cancelled state (something like g_cancellable_is_cancelled)?

Heres a stripped down version of my implementation:

static void
counter_task (GTask        *task,
              GObject      *source_object,
              gpointer      task_data,
              GCancellable *cancellable)
{
  MyObject *source = MY_OBJECT (source_object);
  TaskData *data = (TaskData *) task_data;

  while (data->cycles_remaining > 0)
  {
    operation_1 (task);

    operation_2 (task);

    operation_3 (task);

    operation_4 (task);

    data->cycles_remaining--;
  }
}

static void
do_counter_task_async (MyObject            *self,
                       GCancellable        *cancellable,
                       GAsyncReadyCallback  callback,
                       gpointer             user_data)
{
  GTask *task = NULL;
  TaskData *data = NULL;

  data = g_new0 (CounterTaskData, 1);
  data->cycles_remaining = self->limit;

  task = g_task_new (self, cancellable, callback, user_data);

  g_task_set_return_on_cancel (task, TRUE);

  g_task_set_task_data (task,
                        data,
                        (GDestroyNotify) task_data_destroy);

  self->measurement_task = task;
  
  g_task_run_in_thread (task, (GTaskThreadFunc) counter_task);
}

static void
counter_task_finish (GObject      *source_object,
                     GAsyncResult *result,
                     gpointer      data)
{
  MyObject *source = MY_OBJECT (source_object);

  g_signal_emit_by_name (source, "task-end");

  g_clear_object (&source->cancellable);
}

// this method is connected to with g_signal_connect on MyObject and invoked from the global default main context
static void
stop_async_task (MyObject *self,
                 gpointer  data)
{
  g_cancellable_cancel (self->cancellable);
}

Apologies if I am missing a few simple checks but could someone point me in the right direction to make this possible?

Thanks

Hi,

Yes, GLib won’t “kill” the task (or the thread) , so we can implement a graceful exit.

In the while loop of your counter_task() function, add something like:

if (g_cancellable_is_cancelled ())
{
    g_task_return_on_cancel ();   /* will call counter_task_finish() asynchronously from the main thread */

    /* terminate the thread */
    return;
}

This got me what I needed, thank you!

For anyone lurking in the future, in my case each operation_n method had a blocking workload that could be cancelled. I ended up with a solution that checks in each operation for the cancellable ‘canceled’ state but postpones the g_return_error_if_cancelled () until the next iteration of the task loop.

static void
operation_1 (GTask *task)
{
    ...
    if (g_cancellable_is_cancelled (cancellable))
        return;
    ...
}

static void
counter_task (GTask        *task,
              GObject      *source_object,
              gpointer      task_data,
              GCancellable *cancellable)
{
  MyObject *source = MY_OBJECT (source_object);
  TaskData *data = (TaskData *) task_data;

  while (data->cycles_remaining > 0)
  {
    if (g_cancellable_is_cancelled (cancellable))
        break;

    operation_1 (task);

    operation_2 (task);

    operation_3 (task);

    operation_4 (task);

    data->cycles_remaining--;
  }

  if (g_cancellable_is_cancelled (cancellable))
      g_task_return_error_if_cancelled (task);
  else
      g_task_return_boolean (task, TRUE);
}

Cheers

1 Like

Note that if your task contains blocking I/O operations, what you are supposed to do is put them in non-blocking mode, and then call g_cancellable_make_pollfd so you can poll on both the fd for your operation and the fd for the cancellable. If the cancellable fd becomes ready first then you can exit the task early.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.