繁体   English   中英

如何在 Python+GTK 应用程序中使用 Gio.Task 异步运行阻塞方法?

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

我想要一个 Python (3) GTK (3) 应用程序工作如下:

图1

图 1. 应用程序视图:A,初始视图。 B,进度视图。 C,午餐就绪视图。 D,午餐烧焦视图。

  1. 用户点击“制作午餐”(视图 A)

  2. 应用程序显示进度视图(视图 B)

  3. 应用在后台启动make_lunch [blocking] 操作

  4. 取决于make_lunch结果:

    1. 应用程序显示午餐就绪视图(视图 C)或
    2. 应用程序显示午餐烧焦视图(视图 D)

我试过的

我首先编写了一个同步应用程序(请参阅下面的应用程序:同步版本)。 在此版本中,当用户单击“制作午餐”按钮时,GUI 在初始视图(视图 A)中被阻止。 只要make_lunch操作持续(5 秒),视图就会被阻止。 从不显示进度视图(视图 B)。 相反,它直接从视图 A 转到任何结果视图 C 或 D,具体取决于阻塞操作的结果。 显然,这一切都是我想要避免的。

如果我正确理解了 GNOME Developer 文档(请参阅线程异步编程),我需要使用Gio.Task (又名GTask )在工作线程中运行阻塞操作。

因此,我开始将这些文档上的 C 示例中给出的异步调用的标准模式转换为 Python(请参阅下面的应用程序:异步版本),但我没有走得太远,因为我不熟悉 Z0D6107B8370CAD143E12F像这样的错误

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

当调用Gio.Task object 的不同方法时,比如

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

我无法从错误消息链接的错误报告中获得太多信息,所以我被卡住了。

应用:同步版

这是一个应用程序的阻塞版本,用作起点,一个app.py文件和一个app-window.ui文件,它们将保存在同一目录中,如下所示:

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

Python 代码(另存为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(另存为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>

运行应用程序需要在环境中安装以下软件:

gobject-introspection
gtk3
python3
pygobject

应用程序:异步版本(不工作)

这添加了AppWindow.make_lunch_asyncAppWindow.make_lunch_finishAppWindow.make_lunch_async_callback方法,试图模仿异步操作的标准模式,但由于注释中指出的错误,甚至无法定义有效的make_lunch_async 不知道如何正确地将 C 翻译成 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)

使用Mazhar Hussain 在 GNOME Discourse 上的回答,我更新了示例应用程序以异步运行阻塞操作,而无需修改 Cook model。 相反,我定义了一个可重用的 AsyncWorker class,它的对象负责在后台运行给定的操作。

"""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)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM