How do you run a blocking method asynchronously with Gio.Task in a Python+GTK app?

I want to have a Python (3) GTK (3) application working as follows:

Figure 1. Views of the application: A, Initial view. B, Progress view. C, Lunch ready view. D, Lunch burnt view.

  1. User clicks on “Make lunch” (view A)

  2. App displays Progress view (view B)

  3. App starts a make_lunch [blocking] operation in the background

  4. Depending on make_lunch result:

    1. App displays Lunch ready view (view C) or
    2. App displays Lunch burnt view (view D)

What I’ve tried

I started by writing a synchronous application (see App: synchronous version below). In this version, the GUI gets blocked in the Initial view (view A) when the user clicks the “Make lunch” button. The view is blocked for as long as the make_lunch operation lasts (5 seconds). The Progress view (view B) is never displayed. Instead, it goes directly from view A to any of the result views, C or D, depending on the outcome of the blocking operation. All this, obviously, is what I want to avoid.

If I understand GNOME Developer documentation correctly (see Threads and Asynchronous programming), I need to use Gio.Task (aka GTask) to run the blocking operation in a worker thread.

So, I started translating the standard pattern for asynchronous calls given in the C examples on those documents into Python (see App: Asynchronous version below), but I didn’t get far because I’m not familiar with C and because I started getting errors like

ValueError: Pointer arguments are restricted to integers,
capsules, and None. See:
https://bugzilla.gnome.org/show_bug.cgi?id=683599

when calling different methods of the Gio.Task object, like

  • task.set_source_tag(self.make_lunch_async)
  • task.set_task_data(task_data)
  • task.run_in_thread(self.make_lunch_async_callback)

I couldn’t get much from the bug report linked from the error messages, so I am stuck.

App: Synchronous version

Here is a blocking version of the application to use as a starting point, an app.py file and an app-window.ui file, which are to be saved in the same directory as follows:

my-blocking-app/
├── app.py
└── app-window.ui

Python code (save as app.py):

import random
import time
import sys
import gi

gi.require_version("Gtk", "3.0")

from gi.repository import Gio, Gtk


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.show_all()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    start_button = Gtk.Template.Child("start-button")
    operation_box = Gtk.Template.Child("operation-box")
    progress_box = Gtk.Template.Child("progress-box")
    spinner = Gtk.Template.Child("spinner")
    success_box = Gtk.Template.Child("success-box")
    success_label = Gtk.Template.Child("success-label")
    failure_box = Gtk.Template.Child("failure-box")
    failure_label = Gtk.Template.Child("failure-label")
    back_button = Gtk.Template.Child("back-button")

    def __init__(self, app):
        super().__init__()
        self.set_application(app)

    @Gtk.Template.Callback()
    def on_start_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(False)
        self.progress_box.set_visible(True)
        self.spinner.start()

        # FIXME: Blocking operation. Run asynchronously.
        cook = Cook()
        result = cook.make_lunch("rice", "lentils", "carrots")

        # Show result.
        self.show_result(result)

    @Gtk.Template.Callback()
    def on_back_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(True)
        self.success_box.set_visible(False)
        self.failure_box.set_visible(False)
        button.set_visible(False)

    def show_result(self, result):
        """Update application according to result."""
        self.progress_box.set_visible(False)
        self.back_button.set_visible(True)

        if isinstance(result, Plate):
            message = "Lunch is ready: {}".format(result)
            self.success_label.set_text(message)
            self.success_box.set_visible(True)
        else:
            message = result.get("Error")
            self.failure_label.set_text(message)
            self.failure_box.set_visible(True)


# MODELS

class Plate():
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __str__(self):
        return ", ".join(self.ingredients)


class Cook():
    def make_lunch(self, *ingredients):
        time.sleep(5)

        outcomes = [
            Plate(ingredients),
            {"Error": "Lunch is burned!!"}
        ]

        return random.choice(outcomes)


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

XML UI (save as app-window.ui):

<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
  <requires lib="gtk+" version="3.24"/>
  <template class="AppWindow" parent="GtkApplicationWindow">
    <property name="can-focus">False</property>
    <property name="window-position">center</property>
    <property name="default-width">300</property>
    <property name="default-height">200</property>
    <child>
      <object class="GtkBox">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <property name="margin-start">12</property>
        <property name="margin-end">12</property>
        <property name="margin-top">12</property>
        <property name="margin-bottom">12</property>
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkBox" id="operation-box">
            <property name="visible">True</property>
            <property name="can-focus">False</property>
            <property name="orientation">vertical</property>
            <property name="spacing">12</property>
            <child>
              <object class="GtkImage">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="icon-name">emoji-food-symbolic</property>
                <property name="icon_size">6</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkLabel">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="label" translatable="yes">Making lunch takes 5 seconds.</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
            <child>
              <object class="GtkButton" id="start-button">
                <property name="label" translatable="yes">Make lunch</property>
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="receives-default">True</property>
                <signal name="clicked" handler="on_start_button_clicked" swapped="no"/>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">2</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
        <child>
          <object class="GtkBox" id="progress-box">
            <property name="can-focus">False</property>
            <property name="no-show-all">True</property>
            <property name="halign">center</property>
            <property name="spacing">12</property>
            <child>
              <object class="GtkImage">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="icon-name">emoji-food-symbolic</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkLabel">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="label" translatable="yes">Making lunch...</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
            <child>
              <object class="GtkSpinner" id="spinner">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="active">True</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">2</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkBox" id="success-box">
            <property name="can-focus">False</property>
            <property name="no-show-all">True</property>
            <property name="orientation">vertical</property>
            <property name="spacing">12</property>
            <child>
              <object class="GtkImage">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="icon-name">emoji-nature-symbolic</property>
                <property name="icon_size">6</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkLabel" id="success-label">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="label" translatable="yes">Lunch is ready!!</property>
                <property name="wrap">True</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">2</property>
          </packing>
        </child>
        <child>
          <object class="GtkBox" id="failure-box">
            <property name="can-focus">False</property>
            <property name="no-show-all">True</property>
            <property name="orientation">vertical</property>
            <property name="spacing">12</property>
            <child>
              <object class="GtkImage">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="icon-name">dialog-error-symbolic</property>
                <property name="icon_size">6</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkLabel" id="failure-label">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="label" translatable="yes">Error: message.</property>
                <property name="wrap">True</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">3</property>
          </packing>
        </child>
        <child>
          <object class="GtkButton" id="back-button">
            <property name="label" translatable="yes">Back</property>
            <property name="can-focus">True</property>
            <property name="receives-default">True</property>
            <property name="no-show-all">True</property>
            <property name="margin-top">12</property>
            <signal name="clicked" handler="on_back_button_clicked" swapped="no"/>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">4</property>
          </packing>
        </child>
      </object>
    </child>
    <child type="titlebar">
      <object class="GtkHeaderBar">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <property name="title" translatable="yes">Async</property>
        <property name="subtitle" translatable="yes">Async operation with GTask</property>
        <property name="show-close-button">True</property>
        <child>
          <placeholder/>
        </child>
        <child>
          <placeholder/>
        </child>
      </object>
    </child>
  </template>
</interface>

Running the app requires the following software in the environment:

gobject-introspection
gtk3
python3
pygobject

App: Asynchronous version (not working)

This adds the methods AppWindow.make_lunch_async, AppWindow.make_lunch_finish and AppWindow.make_lunch_async_callback, trying to mimic the standard pattern for async operation, but couldn’t even define a working make_lunch_async because of the errors indicated in the comments. Don’t know how to properly translate the C into Python.

import random
import time
import sys
import gi

gi.require_version("Gtk", "3.0")

from gi.repository import Gio, Gtk


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.show_all()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    start_button = Gtk.Template.Child("start-button")
    operation_box = Gtk.Template.Child("operation-box")
    progress_box = Gtk.Template.Child("progress-box")
    spinner = Gtk.Template.Child("spinner")
    success_box = Gtk.Template.Child("success-box")
    success_label = Gtk.Template.Child("success-label")
    failure_box = Gtk.Template.Child("failure-box")
    failure_label = Gtk.Template.Child("failure-label")
    back_button = Gtk.Template.Child("back-button")

    def __init__(self, app):
        super().__init__()
        self.set_application(app)

    @Gtk.Template.Callback()
    def on_start_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(False)
        self.progress_box.set_visible(True)
        self.spinner.start()

        # Make lunch asynchronously.
        self.make_lunch_async(
            Cook(),
            ("rice", "lentils", "carrots"),
            None,  # Operation is not cancellable.
            self.make_lunch_async_callback,
            None   # No aditional data for the callback
        )

    def make_lunch_async_callback(self, task, source_object, task_data, cancellable):
        """Handle the result of the async operation."""
        cook = task_data.get("cook")
        ingredients = task_data.get("ingredients")
        result = cook.make_lunch(*ingredients)

        # Show result (should I call this here?).
        self.show_result(result)

    def make_lunch_async(self, cook, ingredients, cancellable, callback, callback_data):
        """Schedule async operation and invoke callback when operation
        is done."""
        task = Gio.Task.new(
            self,
            cancellable,
            callback,
            callback_data
        )
        task.set_source_tag(self.make_lunch_async)  # FIXME.
        # Previous line fails with:
        #
        # ValueError: Pointer arguments are restricted to integers,
        # capsules, and None. See:
        # https://bugzilla.gnome.org/show_bug.cgi?id=683599

        # Cancellation should be handled manually using mechanisms
        # specific to the blocking function.
        task.set_return_on_cancel(False)

        # Set up a closure containing the call’s parameters. Copy them
        # to avoid locking issues between the calling thread and the
        # worker thread.
        task_data = {"cook": cook, "ingredients": ingredients}
        task.set_task_data(task_data)  # FIXME.
        # Previous line fails with:
        #
        # ValueError: Pointer arguments are restricted to integers,
        # capsules, and None. See:
        # https://bugzilla.gnome.org/show_bug.cgi?id=683599

        # Run the task in a worker thread and return immediately while
        # that continues in the background. When it’s done it will call
        # @callback in the current thread default main context.
        task.run_in_thread(self.make_lunch_async_callback)

    def make_lunch_finish(self, result, error):
        """What's the purpose of this method."""
        pass

    @Gtk.Template.Callback()
    def on_back_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(True)
        self.success_box.set_visible(False)
        self.failure_box.set_visible(False)
        button.set_visible(False)

    def show_result(self, result):
        """Update application according to result."""
        self.progress_box.set_visible(False)
        self.back_button.set_visible(True)

        if isinstance(result, Plate):
            message = "Lunch is ready: {}".format(result)
            self.success_label.set_text(message)
            self.success_box.set_visible(True)
        else:
            message = result.get("Error")
            self.failure_label.set_text(message)
            self.failure_box.set_visible(True)


# MODELS

class Plate():
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __str__(self):
        return ", ".join(self.ingredients)


class Cook():
    def make_lunch(self, *ingredients):
        time.sleep(5)

        outcomes = [
            Plate(ingredients),
            {"Error": "Lunch is burned!!"}
        ]

        return random.choice(outcomes)


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)
1 Like

I’ve been struggling with this too. It seems like python bindings for Gio.Task do not work as expected. Anyway, I’ve managed to run background tasks using Gio.Task now. This is what I understand so far.

  1. Both make_lunch_async and make_lunch_finish should be part of the same class that defines make_lunch i.e. Cook class.

  2. A class that defines async functions and uses Gio.Task (in this case Cook class) should derive from GObject.Object.

  3. Probably the name make_lunch_async_callback should not be used for a function because there are two different functions called by make_lunch_async that do very different things

    1. One is called to prepare and run make_lunch in a thread. I’ve named this function _make_lunch_thread_callback.

      • This function should be private i.e. its name should start with an underscore.
      • This should be part of the same class which defines make_lunch_async (in this case Cook class).
      • This function should not do any UI changes.
    2. The other is called when previously mentioned function (_make_lunch_thread_callback) returns. I’ve named this function on_make_lunch_finished.

      • This is primarily used to update the UI according to the outcome.
      • This should be part of the same class in which we call make_lunch_async (in this case AppWindow class).
  4. Function make_lunch_finish is responsible for retrieving the return value from our task.

  5. The word result should not be used for outcome/return_value of make_lunch. Using it can be confusing since it is used by the documentation for function arguments of types that implement Gio.AsyncResult interface. Gio.Task also implements it.

    So, I use result as argument name only for our Gio.Task instance and only when it is being used as a Gio.AsyncResult.

    And I use only the word outcome to represent the outcome of make_lunch.

See code comments for more information.

#!/bin/env python3

import random
import time
import sys
import gi

gi.require_version("Gtk", "3.0")

from gi.repository import GObject, Gio, Gtk


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.show_all()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    start_button = Gtk.Template.Child("start-button")
    operation_box = Gtk.Template.Child("operation-box")
    progress_box = Gtk.Template.Child("progress-box")
    spinner = Gtk.Template.Child("spinner")
    success_box = Gtk.Template.Child("success-box")
    success_label = Gtk.Template.Child("success-label")
    failure_box = Gtk.Template.Child("failure-box")
    failure_label = Gtk.Template.Child("failure-label")
    back_button = Gtk.Template.Child("back-button")

    def __init__(self, app):
        super().__init__()
        self.set_application(app)

    @Gtk.Template.Callback()
    def on_start_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(False)
        self.progress_box.set_visible(True)
        self.spinner.start()

        cook = Cook()
        cook.make_lunch_async(self.on_make_lunch_finished, ("rice", "lentils", "carrots"))

    @Gtk.Template.Callback()
    def on_back_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(True)
        self.success_box.set_visible(False)
        self.failure_box.set_visible(False)
        button.set_visible(False)

    def on_make_lunch_finished(self, source_object, result, user_data):
        '''Get outcome from make_lunch_async and call show_outcome'''
        cook = source_object

        outcome = cook.make_lunch_finish(result)

        if outcome != -1:
            self.show_outcome(outcome)

        self.spinner.stop()

    def show_outcome(self, outcome):
        """Update application according to the outcome."""
        self.progress_box.set_visible(False)
        self.back_button.set_visible(True)

        if isinstance(outcome, Plate):
            message = "Lunch is ready: {}".format(outcome)
            self.success_label.set_text(message)
            self.success_box.set_visible(True)
        else:
            message = outcome.get("Error")
            self.failure_label.set_text(message)
            self.failure_box.set_visible(True)


# MODELS

class Plate():
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __str__(self):
        return ", ".join(self.ingredients)


class Cook(GObject.Object):
    def __init__ (self):
        super().__init__()

        # Required to hold actual task data since we cannot
        # set task data directly.
        self.pool = {}

    def make_lunch(self, *ingredients):
        time.sleep(5)

        outcomes = [
            Plate(ingredients),
            {"Error": "Lunch is burned!!"}
        ]

        return random.choice(outcomes)

    def make_lunch_async (self, callback, ingredients):
        '''Make lunch asynchronously (in the background)'''

        task = Gio.Task.new(self, None, callback, None)
        task.set_return_on_cancel(False)

        # Since we cannot set task data to anything accept None or an integer, we
        # set task data to id of our actual data (ingredients) and store the actual
        # data in an instance-wide dictionary (self.pool) with key being the id of our data.
        # Also, we need to remove our data from the dictionary when we're done with it.
        ingredients_id = id(ingredients)
        task.set_task_data(ingredients_id, lambda x: self.pool.pop(ingredients_id))
        self.pool[ingredients_id] = ingredients

        task.run_in_thread(self._make_lunch_thread_callback)

    # Below function is only for internal use. So, it should be private
    # i.e. it's name should start with an underscore
    def _make_lunch_thread_callback (self, task, source_object, task_data, cancellable):
        '''Called by make_lunch_async to make lunch in a thread'''

        if task.return_error_if_cancelled():
            return
        
        # Value of argument 'task_data' is always None for some reason.
        # So, we need to get task_data using task.get_task_data() method.
        task_data = task.get_task_data()

        # Task data is just an id of the actual data. So, we need to get
        # the actual data from our instance-wide dictionary
        ingredients = self.pool[task_data]

        outcome = self.make_lunch(*ingredients)

        task.return_value(outcome)

    def make_lunch_finish (self, result):
        '''Get outcome of making_lunch'''
        
        # Note that Gio.Task.is_valid is a class method. So, no matter if
        # you call it as Gio.Task.is_valid or as result.is_valid, you will
        # have to give it `result` as the first argument
        if not Gio.Task.is_valid(result, self):
            return -1

        return result.propagate_value().value


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)
2 Likes

@mazharhussain Thanks a lot! This is very helpful :slightly_smiling_face:

I’m going to peruse the whole thing again and try to apply the solution to my real-world application.

1 Like

Using @mazharhussain answer, I updated the example application to run the blocking operation asynchronously without modifying the Cook model. Instead, I defined a reusable AsyncWorker class, whose objects take care of running a given operation in the background.


"""Running an asynchronous operation from GTK App with Gio.Task."""

import random
import time
import sys
import gi

gi.require_version("Gtk", "3.0")

from gi.repository import Gio, GObject, Gtk


# VIEWS

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id="org.example.app",
            flags=Gio.ApplicationFlags.FLAGS_NONE
        )

    def do_activate(self):
        # Show default application window.
        window = AppWindow(self)
        self.add_window(window)
        window.show_all()


@Gtk.Template(filename="app-window.ui")
class AppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = "AppWindow"

    start_button = Gtk.Template.Child("start-button")
    operation_box = Gtk.Template.Child("operation-box")
    progress_box = Gtk.Template.Child("progress-box")
    spinner = Gtk.Template.Child("spinner")
    success_box = Gtk.Template.Child("success-box")
    success_label = Gtk.Template.Child("success-label")
    failure_box = Gtk.Template.Child("failure-box")
    failure_label = Gtk.Template.Child("failure-label")
    back_button = Gtk.Template.Child("back-button")

    def __init__(self, app):
        super().__init__()
        self.set_application(app)

    @Gtk.Template.Callback()
    def on_start_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(False)
        self.progress_box.set_visible(True)
        self.spinner.start()

        # Make lunch asynchronously.
        cook = Cook()
        ingredients = ("rice", "lentils", "carrots")
        async_worker = AsyncWorker(
            operation=cook.make_lunch,
            operation_inputs=ingredients,
            operation_callback=self.on_lunch_finished
        )
        async_worker.start()

    @Gtk.Template.Callback()
    def on_back_button_clicked(self, button):
        """Handle the BUTTON's clicked signal."""
        self.operation_box.set_visible(True)
        self.success_box.set_visible(False)
        self.failure_box.set_visible(False)
        button.set_visible(False)

    def on_lunch_finished(self, worker, result, handler_data):
        """Handle the RESULT of the asynchronous operation performed by
        WORKER.

        WORKER (AsyncWorker)
          The worker performing the asynchronous operation.

        RESULT (Gio.AsyncResult)
          The asynchronous result of the asynchronous operation.

        HANDLER_DATA (None)
          Additional data passed to this handler by the worker when the
          job is done. It should be None in this case.

        """
        outcome = worker.return_value(result)
        self.show_outcome(outcome)

    def show_outcome(self, outcome):
        """Update application according to the given outcome."""
        self.spinner.stop()
        self.progress_box.set_visible(False)
        self.back_button.set_visible(True)

        if isinstance(outcome, Plate):
            message = "Lunch is ready: {}".format(outcome)
            self.success_label.set_text(message)
            self.success_box.set_visible(True)
        else:
            message = outcome.get("Error")
            self.failure_label.set_text(message)
            self.failure_box.set_visible(True)


# MODELS

class Plate():
    def __init__(self, ingredients):
        self.ingredients = ingredients

    def __str__(self):
        return ", ".join(self.ingredients)


class Cook():
    def make_lunch(self, *ingredients):
        time.sleep(5)

        outcomes = [
            Plate(ingredients),
            {"Error": "Lunch is burned!!"}
        ]

        return random.choice(outcomes)


# ASYNCHRONOUS WORKER

class AsyncWorker(GObject.Object):
    """Represents an asynchronous worker.

    An async worker's job is to run a blocking operation in the
    background using a Gio.Task to avoid blocking the app's main thread
    and freezing the user interface.

    The terminology used here is closely related to the Gio.Task API.

    There are two ways to specify the operation that should be run in
    the background:

    1. By passing the blocking operation (a function or method) to the
       constructor.
    2. By defining the work() method in a subclass.

    An example of (1) can be found in AppWindow.on_start_button_clicked.

    Constructor parameters:

    OPERATION (callable)
      The function or method that needs to be run asynchronously. This
      is only necessary when using a direct instance of AsyncWorker, not
      when using an instance of a subclass of AsyncWorker, in which case
      an AsyncWorker.work() method must be defined by the subclass
      instead.

    OPERATION_INPUTS (tuple)
      Input data for OPERATION, if any.

    OPERATION_CALLBACK (callable)
      A function or method to call when the OPERATION is complete.

      See AppWindow.on_lunch_finished for an example of such callback.

    OPERATION_CALLBACK_INPUTS (tuple)
      Optional. Additional input data for OPERATION_CALLBACK.

    CANCELLABLE (Gio.Cancellable)
      Optional. It defaults to None, meaning that the blocking
      operation is not cancellable.

    """
    def __init__(
            self,
            operation=None,
            operation_inputs=(),
            operation_callback=None,
            operation_callback_inputs=(),
            cancellable=None
    ):
        super().__init__()
        self.operation = operation
        self.operation_inputs = operation_inputs
        self.operation_callback = operation_callback
        self.operation_callback_inputs = operation_callback_inputs
        self.cancellable = cancellable

        # Holds the actual data referenced from the Gio.Task created
        # in the AsyncWorker.start method.
        self.pool = {}

    def start(self):
        """Schedule the blocking operation to be run asynchronously.

        The blocking operation is either self.operation or self.work,
        depending on how the AsyncWorker was instantiated.

        This method corresponds to the function referred to as
        "blocking_function_async" in GNOME Developer documentation.

        """
        task = Gio.Task.new(
            self,
            self.cancellable,
            self.operation_callback,
            self.operation_callback_inputs
        )

        if self.cancellable is None:
            task.set_return_on_cancel(False)  # The task is not cancellable.

        data_id = id(self.operation_inputs)
        self.pool[data_id] = self.operation_inputs
        task.set_task_data(
            data_id,
            # FIXME: Data destroyer function always gets None as argument.
            #
            # This function is supposed to take as an argument the
            # same value passed as data_id to task.set_task_data, but
            # when the destroyer function is called, it seems it always
            # gets None as an argument instead. That's why the "key"
            # parameter is not being used in the body of the anonymous
            # function.
            lambda key: self.pool.pop(data_id)
        )

        task.run_in_thread(self._thread_callback)

    def _thread_callback(self, task, worker, task_data, cancellable):
        """Run the blocking operation in a worker thread."""
        # FIXME: task_data is always None for Gio.Task.run_in_thread callback.
        #
        # The value passed to this callback as task_data always seems to
        # be None, so we get the data for the blocking operation as
        # follows instead.
        data_id = task.get_task_data()
        data = self.pool.get(data_id)

        # Run the blocking operation.
        if self.operation is None:  # Assume AsyncWorker was extended.
            outcome = self.work(*data)
        else:  # Assume AsyncWorker was instantiated directly.
            outcome = self.operation(*data)

        task.return_value(outcome)

    def return_value(self, result):
        """Return the value of the operation that was run
        asynchronously.

        This method corresponds to the function referred to as
        "blocking_function_finish" in GNOME Developer documentation.

        This method is called from the view where the asynchronous
        operation is started to update the user interface according
        to the resulting value.

        RESULT (Gio.AsyncResult)
          The asyncronous result of the blocking operation that is
          run asynchronously.

        RETURN VALUE (object)
          Any of the return values of the blocking operation. If
          RESULT turns out to be invalid, return an error dictionary
          in the form

          {"AsyncWorkerError": "Gio.Task.is_valid returned False."}

        """
        value = None

        if Gio.Task.is_valid(result, self):
            value = result.propagate_value().value
        else:
            error = "Gio.Task.is_valid returned False."
            value = {"AsyncWorkerError": error}

        return value


# RUN APP
if __name__ == "__main__":
    app = App()
    exit_status = app.run(sys.argv)
    sys.exit(exit_status)

Also, I’m now using this solution in my real-world application and it has worked well so far. Previously, I was using threading.Thread instead and emmiting signals from the worker threads, but was getting GTK critical errors and segmentation faults sometimes. Using GTask, those problems are gone.

1 Like

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