[gnome-devel-docs] programming-guidelines: Add a page on asynchronous programming
- From: David King <davidk src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-devel-docs] programming-guidelines: Add a page on asynchronous programming
- Date: Mon, 11 Feb 2019 11:00:29 +0000 (UTC)
commit 1dca93536254547bfebfdba0bed2b742ff32f38f
Author: Philip Withnall <philip withnall collabora co uk>
Date: Tue Feb 24 15:19:26 2015 +0000
programming-guidelines: Add a page on asynchronous programming
This covers various techniques for using and defining GAsyncResult-style
asynchronous methods, based around the use of examples.
https://bugzilla.gnome.org/show_bug.cgi?id=745092
programming-guidelines/C/async-programming.page | 1200 +++++++++++++++++++++++
programming-guidelines/C/main-contexts.page | 8 +-
programming-guidelines/C/threading.page | 2 +-
programming-guidelines/Makefile.am | 1 +
4 files changed, 1208 insertions(+), 3 deletions(-)
---
diff --git a/programming-guidelines/C/async-programming.page b/programming-guidelines/C/async-programming.page
new file mode 100644
index 00000000..c61b91b1
--- /dev/null
+++ b/programming-guidelines/C/async-programming.page
@@ -0,0 +1,1200 @@
+<page xmlns="http://projectmallard.org/1.0/"
+ xmlns:its="http://www.w3.org/2005/11/its"
+ xmlns:xi="http://www.w3.org/2003/XInclude"
+ type="topic"
+ id="async-programming">
+
+ <info>
+ <link type="guide" xref="index#specific-how-tos"/>
+
+ <credit type="author copyright">
+ <name>Philip Withnall</name>
+ <email its:translate="no">philip withnall collabora co uk</email>
+ <years>2015</years>
+ </credit>
+
+ <include href="cc-by-sa-3-0.xml" xmlns="http://www.w3.org/2001/XInclude"/>
+
+ <desc>
+ Use of GLib-style asynchronous methods in various situations
+ </desc>
+ </info>
+
+ <title>Asynchronous Programming</title>
+
+ <synopsis>
+ <title>Summary</title>
+
+ <list>
+ <item><p>
+ Use asynchronous calls in preference to synchronous calls or explicit
+ use of threads (<link xref="#concepts"/>)
+ </p></item>
+ <item><p>
+ Learn and follow the GLib pattern for declaring asynchronous APIs
+ (<link xref="#api-pattern"/>)
+ </p></item>
+ <item><p>
+ Place callbacks from asynchronous functions in order down the file, so
+ control flow is easy to follow (<link xref="#single-call"/>)
+ </p></item>
+ <item><p>
+ Use the presence of a
+ <link href="https://developer.gnome.org/gio/stable/GTask.html"><code>GTask</code></link>
+ or <link
href="https://developer.gnome.org/gio/stable/GCancellable.html"><code>GCancellable</code></link>
+ to indicate whether an operation is ongoing
+ (<link xref="#single-call"/>, <link xref="#gtask"/>)
+ </p></item>
+ <item><p>
+ If running operations in parallel, track how many operations are yet to
+ start, and how many are yet to finish — the overall operation is
+ complete once both counts are zero
+ (<link xref="#parallel"/>)
+ </p></item>
+ <item><p>
+ Separate state for operations into ‘task data’ structures for
+ <link href="https://developer.gnome.org/gio/stable/GTask.html"><code>GTask</code>s</link>,
+ allowing operations to be reused more easily without needing changes to
+ global state handling (<link xref="#gtask"/>)
+ </p></item>
+ <item><p>
+ Consider how asynchronous methods on an object instance interact with
+ finalization of that instance (<link xref="#lifetimes"/>)
+ </p></item>
+ </list>
+ </synopsis>
+
+ <section id="concepts">
+ <title>Concepts</title>
+
+ <p>
+ GLib supports <em>asynchronous</em> programming, where long-running
+ operations can be started, run ‘in the background’, and a callback invoked
+ when they are finished and their results are available. This is in direct
+ contrast to <em>synchronous</em> long-running operations, which are a
+ single function call which blocks program control flow until complete.
+ </p>
+
+ <p>
+ As discussed in <link xref="main-contexts"/> and
+ <link xref="threading#when-to-use-threading"/>, asynchronous operations
+ should be favoured over synchronous ones and over explicit use of
+ threading. They do not block the main context like sychronous operations
+ do; and are easier to use correctly than threads. They often also have a
+ lower performance penalty than spawning a thread and sending work to it.
+ </p>
+ </section>
+
+ <section id="api-pattern">
+ <title>API Pattern</title>
+
+ <p>
+ Asynchronous calls follow a standard pattern in GLib code. For an
+ operation named <code>load_data</code> on the <code>File</code> class in
+ the <code>Foo</code> namespace, there will be:
+ </p>
+ <list>
+ <item>
+ <code mime="text/x-csrc">
+foo_file_load_data_async (FooFile *self,
+ …,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)</code>
+ </item>
+ <item>
+ <code mime="text/x-csrc">
+foo_file_load_data_finish (FooFile *self,
+ GAsyncResult *result,
+ …,
+ GError **error)</code>
+ </item>
+ </list>
+
+ <p>
+ The <code>…</code> parameters to <code>foo_file_load_data_async()</code>
+ are those specific to the operation — in this case, perhaps the size of a
+ buffer to load into. Similarly for
+ <code>foo_file_load_data_finish()</code> they are the operation-specific
+ return values — perhaps a location to return a content type string in this
+ case.
+ </p>
+
+ <p>
+ When <code>foo_file_load_data_async()</code> is called, it schedules the
+ load operation in the background (as a new file descriptor on the
+ <link xref="main-contexts"><code>GMainContext</code></link> or as a worker
+ thread, for example), then returns without blocking.
+ </p>
+
+ <p>
+ When the operation is complete, the <code>callback</code> is executed in
+ the same <code>GMainContext</code> as the original asynchronous call. The
+ callback is invoked <em>exactly</em> once, whether the operation succeeded
+ or failed.
+ </p>
+
+ <p>
+ From the callback, <code>foo_file_load_data_finish()</code> may be called
+ by the user’s code to retrieve return values and error details, passing
+ the <link
href="https://developer.gnome.org/gio/stable/GAsyncResult.html"><code>GAsyncResult</code></link>
+ instance which was passed to the callback.
+ </p>
+ </section>
+
+ <section id="lifetimes">
+ <title>Operation Lifetimes</title>
+
+ <p>
+ When writing asynchronous operations, it is common to write them as
+ methods of a class. In this case, it is important to define how ongoing
+ operations on a class instance interact with finalization of that
+ instance. There are two approaches:
+ </p>
+
+ <terms>
+ <item>
+ <title>Strong</title>
+ <p>
+ The ongoing operation keeps a reference to the class instance, forcing
+ it to remain alive for the duration of the operation. The class should
+ provide some kind of ‘close’ or ‘cancel’ method which can be used by
+ other classes to force cancellation of the operation and allow that
+ instance to be finalized.
+ </p>
+ </item>
+
+ <item>
+ <title>Weak</title>
+ <p>
+ The ongoing operation does <em>not</em> keep a reference to the class
+ instance, and the class cancels the operation (using
+ <link
href="https://developer.gnome.org/gio/stable/GCancellable.html#g-cancellable-cancel"><code>g_cancellable_cancel()</code></link>)
+ in its dispose function.
+ </p>
+ </item>
+ </terms>
+
+ <p>
+ Which approach is used depends on the class’ design. A class which wraps
+ a particular operation (perhaps a <code>MyFileTransfer</code> class, for
+ example) might want to use the <em style="strong">weak</em> approach.
+ A class which manages multiple network connections and asynchronous
+ operations on them may use the <em style="strong">strong</em> approach
+ instead. Due to incoming network connections, for example, it might not be
+ in complete control of the scheduling of its asynchronous calls, so the
+ weak approach would not be appropriate — any code dropping a reference to
+ the object could not be sure it was not accidentally killing a new network
+ connection.
+ </p>
+ </section>
+
+ <section id="async-examples">
+ <title>Examples of Using Asynchronous Functions</title>
+
+ <p>
+ It is often the case that multiple asynchronous calls need to be used to
+ complete an operation. For example, opening a file for reading, then
+ performing a couple of reads, and then closing the file. Or opening
+ several network sockets in parallel and waiting until they are all open
+ before continuing with other work. Some examples of these situations are
+ given below.
+ </p>
+
+ <section id="single-call">
+ <title>Single Operation</title>
+
+ <p>
+ A single asynchronous call requires two functions: one to start the
+ operation, and one to complete it. In C, the demanding part of
+ performing an asynchronous call is correctly storing state between these
+ two functions, and handling changes to that state in the time between
+ those two functions being called. For example, cancellation of an
+ ongoing asynchronous call is a state change, and if not implemented
+ carefully, any UI updates (for example) made when cancelling an
+ operation will be undone by updates in the operation’s callback.
+ </p>
+
+ <example>
+ <p>
+ This example demonstrates copying a file from one location in the file
+ system to another. The key principles demonstrated here are:
+ </p>
+ <list>
+ <item><p>
+ Placing the <code>copy_button_clicked_cb()</code> (start) and
+ <code>copy_finish_cb()</code> (finish) functions in order by using
+ a forward declaration for <code>copy_finish_cb()</code>. This means
+ the control flow continues linearly down the file, rather than
+ getting to the bottom of <code>copy_button_clicked_cb()</code> and
+ resuming in <code>copy_finish_cb()</code> somewhere else in the
+ file.
+ </p></item>
+ <item><p>
+ Use of a
+ <link
href="https://developer.gnome.org/gio/stable/GCancellable.html"><code>GCancellable</code></link>
+ to allow cancelling the operation
+ after it has started. The code in
+ <code>cancel_button_clicked_cb()</code> is very simple: as the
+ <code>copy_finish_cb()</code> callback is <em>guaranteed</em> to be
+ invoked when the operation completes (even when completing early
+ due to cancellation), all the UI and state updates for cancellation
+ can be handled there, rather than in
+ <code>cancel_button_clicked_cb()</code>.
+ </p></item>
+ <item><p>
+ An operation is ongoing exactly while
+ <code>MyObjectPrivate.copy_cancellable</code> is
+ non-<code>NULL</code>, making it easy to track running operations.
+ Note that this means only one file copy operation can be started
+ via <code>copy_button_clicked_cb()</code> at a time. One
+ <code>GCancellable</code> cannot easily be used for multiple
+ operations like this.
+ </p></item>
+ </list>
+
+ <code mime="text/x-csrc" style="valid">
+static void
+copy_finish_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+copy_button_clicked_cb (GtkButton *button
+ gpointer user_data)
+{
+ MyObjectPrivate *priv;
+ GFile *source = NULL, *destination = NULL; /* owned */
+
+ priv = my_object_get_instance_private (MY_OBJECT (user_data));
+
+ /* Operation already in progress? */
+ if (priv->copy_cancellable != NULL)
+ {
+ g_debug ("Copy already in progress.");
+ return;
+ }
+
+ /* Build source and destination file paths. */
+ source = g_file_new_for_path (/* some path generated from UI */);
+ destination = g_file_new_for_path (/* some other path generated from UI */);
+
+ /* Set up a cancellable. */
+ priv->copy_cancellable = g_cancellable_new ();
+
+ g_file_copy_async (source, destination, G_FILE_COPY_NONE, G_PRIORITY_DEFAULT,
+ priv->copy_cancellable, NULL, NULL,
+ copy_finish_cb, user_data);
+
+ g_object_unref (destination);
+ g_object_unref (source);
+
+ /* Update UI to show copy is in progress. */
+ …
+}
+
+static void
+copy_finish_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObjectPrivate *priv;
+ GFile *source; /* unowned */
+ GError *error = NULL;
+
+ source = G_FILE (source_object);
+ priv = my_object_get_instance_private (MY_OBJECT (user_data));
+
+ /* Handle completion of the operation. */
+ g_file_copy_finish (source, result, &error);
+
+ if (error != NULL &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ /* Should update the UI to signal failure.
+ * Ignore failure due to cancellation. */
+ g_warning ("Failed to copy file: %s", error->message);
+ }
+
+ g_clear_error (&error);
+
+ /* Clear the cancellable to signify the operation has finished. */
+ g_clear_object (&priv->copy_cancellable);
+
+ /* Update UI to show copy as complete. */
+ …
+}
+
+static void
+cancel_button_clicked_cb (GtkButton *button,
+ gpointer user_data)
+{
+ MyObjectPrivate *priv;
+ GFile *source = NULL, *destination = NULL; /* owned */
+
+ priv = my_object_get_instance_private (MY_OBJECT (user_data));
+
+ /* Operation in progress? No-op if @copy_cancellable is %NULL. */
+ g_cancellable_cancel (priv->copy_cancellable);
+}
+
+static void
+my_object_dispose (GObject *obj)
+{
+ MyObjectPrivate *priv;
+
+ priv = my_object_get_instance_private (MY_OBJECT (obj));
+
+ /* Cancel any ongoing copy operation.
+ *
+ * This ensures that if #MyObject is disposed part-way through a copy, the
+ * callback doesn’t get invoked with an invalid #MyObject pointer. */
+ g_cancellable_cancel (priv->copy_cancellable);
+
+ /* Do other dispose calls here. */
+ …
+
+ /* Chain up. */
+ G_OBJECT_CLASS (my_object_parent_class)->dispose (obj);
+}</code>
+
+ <p>
+ For comparison, here is the same code implemented using the
+ <em>synchronous</em> version of
+ <link
href="https://developer.gnome.org/gio/stable/GFile.html#g-file-copy"><code>g_file_copy()</code></link>.
+ Note how the order of statements is almost identical. Cancellation
+ cannot be supported here, as the UI is blocked from receiving ‘click’
+ events on the cancellation button while the copy is ongoing, so
+ <code>NULL</code> is passed to the <code>GCancellable</code>
+ parameter. This is the main reason why this code should <em>not</em>
+ be used in practice.
+ </p>
+
+ <code mime="text/x-csrc" style="invalid">
+static void
+copy_button_clicked_cb (GtkButton *button
+ gpointer user_data)
+{
+ MyObjectPrivate *priv;
+ GFile *source = NULL, *destination = NULL; /* owned */
+
+ priv = my_object_get_instance_private (MY_OBJECT (user_data));
+
+ /* Build source and destination file paths. */
+ source = g_file_new_for_path (/* some path generated from UI */);
+ destination = g_file_new_for_path (/* some other path generated from UI */);
+
+ g_file_copy (source, destination, G_FILE_COPY_NONE,
+ NULL /* cancellable */, NULL, NULL,
+ &error);
+
+ g_object_unref (destination);
+ g_object_unref (source);
+
+ /* Handle completion of the operation. */
+ if (error != NULL)
+ {
+ /* Should update the UI to signal failure.
+ * Ignore failure due to cancellation. */
+ g_warning ("Failed to copy file: %s", error->message);
+ }
+
+ g_clear_error (&error);
+
+ /* Update UI to show copy as complete. */
+ …
+}</code>
+ </example>
+ </section>
+
+ <section id="series">
+ <title>Operations in Series</title>
+
+ <p>
+ A common situation is to run multiple asynchronous operations in series,
+ when each operation depends on the previous one completing.
+ </p>
+
+ <example>
+ <p>
+ In this example, the application reads a socket address from a file,
+ opens a connection to that address, reads a message, and then
+ finishes.
+ </p>
+
+ <p>
+ Key points in this example are:
+ </p>
+ <list>
+ <item><p>
+ Each callback is numbered consistently, and they are all placed in
+ order in the file so the code follows sequentially.
+ </p></item>
+ <item><p>
+ As in <link xref="#single-call"/>, a single
+ <code>GCancellable</code> indicates that the series of operations is
+ ongoing. Cancelling it aborts the entire sequence.
+ </p></item>
+ <item><p>
+ As in <link xref="#single-call"/>, the pending operation is
+ cancelled if the owning <code>MyObject</code> instance is disposed,
+ to prevent callbacks being called later with an invalid
+ <code>MyObject</code> pointer.
+ </p></item>
+ </list>
+
+ <p>
+ <link xref="#gtask"/> gives a version of this example wrapped in a
+ <link href="https://developer.gnome.org/gio/stable/GTask.html"><code>GTask</code></link>
+ for convenience.
+ </p>
+
+ <code mime="text/x-csrc" style="valid">
+static void
+connect_to_server_cb1 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void
+connect_to_server_cb2 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void
+connect_to_server_cb3 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+connect_to_server (MyObject *self)
+{
+ MyObjectPrivate *priv;
+ GFile *address_file = NULL; /* owned */
+
+ priv = my_object_get_instance_private (self);
+
+ if (priv->connect_cancellable != NULL)
+ {
+ /* Already connecting. */
+ return;
+ }
+
+ /* Set up a cancellable. */
+ priv->connect_cancellable = g_cancellable_new ();
+
+ /* Read the socket address. */
+ address_file = build_address_file ();
+ g_file_load_contents_async (address_file, priv->connect_cancellable,
+ connect_to_server_cb1, self);
+ g_object_unref (address_file);
+}
+
+static void
+connect_to_server_cb1 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObject *self;
+ MyObjectPrivate *priv;
+ GFile *address_file; /* unowned */
+ gchar *address = NULL; /* owned */
+ gsize address_size = 0;
+ GInetAddress *inet_address = NULL; /* owned */
+ GInetSocketAddress *inet_socket_address = NULL; /* owned */
+ guint16 port = 123;
+ GSocketClient *socket_client = NULL; /* owned */
+ GError *error = NULL;
+
+ address_file = G_FILE (source_object);
+ self = MY_OBJECT (user_data);
+ priv = my_object_get_instance_private (self);
+
+ /* Finish loading the address. */
+ g_file_load_contents_finish (address_file, result, &address,
+ &address_size, NULL, &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+ /* Parse the address. */
+ inet_address = g_inet_address_new_from_string (address);
+
+ if (inet_address == NULL)
+ {
+ /* Error. */
+ g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ "Invalid address ‘%s’.", address);
+ goto done;
+ }
+
+ inet_socket_address = g_inet_socket_address_new (inet_address, port);
+
+ /* Connect to the given address. */
+ socket_client = g_socket_client_new ();
+
+ g_socket_client_connect_async (socket_client,
+ G_SOCKET_CONNECTABLE (inet_socket_address),
+ priv->connect_cancellable,
+ connect_to_server_cb2,
+ self);
+
+done:
+ if (error != NULL)
+ {
+ /* Stop the operation. */
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_warning ("Failed to load server address: %s", error->message);
+ }
+
+ g_clear_object (&priv->connect_cancellable);
+ g_error_free (error);
+ }
+
+ g_free (address);
+ g_clear_object (&inet_address);
+ g_clear_object (&inet_socket_address);
+ g_clear_object (&socket_client);
+}
+
+static void
+connect_to_server_cb2 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObject *self;
+ MyObjectPrivate *priv;
+ GSocketClient *socket_client; /* unowned */
+ GSocketConnection *connection = NULL; /* owned */
+ GInputStream *input_stream; /* unowned */
+ GError *error = NULL;
+
+ socket_client = G_SOCKET_CLIENT (source_object);
+ self = MY_OBJECT (user_data);
+ priv = my_object_get_instance_private (self);
+
+ /* Finish connecting to the socket. */
+ connection = g_socket_client_connect_finish (socket_client, result,
+ &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+ /* Store a reference to the connection so it is kept open while we read from
+ * it: #GInputStream does not keep a reference to a #GIOStream which contains
+ * it. */
+ priv->connection = g_object_ref (connection);
+
+ /* Read a message from the connection. This uses a single buffer stored in
+ * #MyObject, meaning that only one connect_to_server() operation can run at
+ * any time. The buffer could instead be allocated dynamically if this is a
+ * problem. */
+ input_stream = g_io_stream_get_input_stream (G_IO_STREAM (connection));
+
+ g_input_stream_read_async (input_stream,
+ priv->message_buffer,
+ sizeof (priv->message_buffer),
+ G_PRIORITY_DEFAULT, priv->connect_cancellable,
+ connect_to_server_cb3, self);
+
+done:
+ if (error != NULL)
+ {
+ /* Stop the operation. */
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_warning ("Failed to connect to server: %s", error->message);
+ }
+
+ g_clear_object (&priv->connect_cancellable);
+ g_clear_object (&priv->connection);
+ g_error_free (error);
+ }
+
+ g_clear_object (&connection);
+}
+
+static void
+connect_to_server_cb3 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObject *self;
+ MyObjectPrivate *priv;
+ GInputStream *input_stream; /* unowned */
+ gssize len = 0;
+ GError *error = NULL;
+
+ input_stream = G_INPUT_STREAM (source_object);
+ self = MY_OBJECT (user_data);
+ priv = my_object_get_instance_private (self);
+
+ /* Finish reading from the socket. */
+ len = g_input_stream_read_finish (input_stream, result, &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+ /* Handle the message. */
+ g_assert_cmpint (len, >=, 0);
+ g_assert_cmpuint ((gsize) len, <=, sizeof (priv->message_buffer));
+
+ handle_received_message (self, priv->message_buffer, len, &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+done:
+ /* Unconditionally mark the operation as finished.
+ *
+ * The streams should automatically close as this
+ * last reference is dropped. */
+ g_clear_object (&priv->connect_cancellable);
+ g_clear_object (&priv->connection);
+
+ if (error != NULL)
+ {
+ /* Warn about the error. */
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_warning ("Failed to read from the server: %s", error->message);
+ }
+
+ g_error_free (error);
+ }
+}
+
+static void
+my_object_dispose (GObject *obj)
+{
+ MyObjectPrivate *priv;
+
+ priv = my_object_get_instance_private (MY_OBJECT (obj));
+
+ /* Cancel any ongoing connection operations.
+ *
+ * This ensures that if #MyObject is disposed part-way through the
+ * connect_to_server() sequence of operations, the sequence gets cancelled and
+ * doesn’t continue with an invalid #MyObject pointer. */
+ g_cancellable_cancel (priv->connect_cancellable);
+
+ /* Do other dispose calls here. */
+ …
+
+ /* Chain up. */
+ G_OBJECT_CLASS (my_object_parent_class)->dispose (obj);
+}</code>
+ </example>
+ </section>
+
+ <section id="parallel">
+ <title>Operations in Parallel</title>
+
+ <p>
+ Another common situation is to run multiple asynchronous operations in
+ parallel, considering the overall operation complete when all its
+ constituents are complete.
+ </p>
+
+ <example>
+ <p>
+ In this example, the application deletes multiple files in parallel.
+ </p>
+
+ <p>
+ Key points in this example are:
+ </p>
+ <list>
+ <item><p>
+ The number of pending asynchronous operations (ones which have
+ started but not yet finished) is tracked as
+ <code>n_deletions_pending</code>. The <code>delete_files_cb()</code>
+ callback only considers the entire operation complete once this
+ reaches zero.
+ </p></item>
+ <item><p>
+ <code>n_deletions_to_start</code> tracks deletion operations being
+ started, in case
+ <link
href="https://developer.gnome.org/gio/stable/GFile.html#g-file-delete-async"><code>g_file_delete_async()</code></link>
+ manages to use a fast path and complete synchronously (without
+ blocking).
+ </p></item>
+ <item><p>
+ As in <link xref="#single-call"/>, all pending deletions are
+ cancelled if the owning <code>MyObject</code> instance is disposed,
+ to prevent callbacks being called later with an invalid
+ <code>MyObject</code> pointer.
+ </p></item>
+ </list>
+
+ <code mime="text/x-csrc" style="valid">
+static void
+delete_files_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+delete_files (MyObject *self,
+ GPtrArray/*<owned GFile*>>*/ *files)
+{
+ MyObjectPrivate *priv;
+ GFile *address_file = NULL; /* owned */
+
+ priv = my_object_get_instance_private (self);
+
+ /* Set up a cancellable if no operation is ongoing already. */
+ if (priv->delete_cancellable == NULL)
+ {
+ priv->delete_cancellable = g_cancellable_new ();
+ priv->n_deletions_pending = 0;
+ priv->n_deletions_total = 0;
+ }
+
+ /* Update internal state, and temporarily set @n_deletions_to_start. This is
+ * used in delete_files_cb() to avoid indicating the overall operation has
+ * completed while deletions are still being started. This can happen if
+ * g_file_delete_async() completes synchronously, for example if there’s a
+ * non-blocking fast path for the given file system. */
+ priv->n_deletions_pending += files->len;
+ priv->n_deletions_total += files->len;
+ priv->n_deletions_to_start = files->len;
+
+ /* Update the UI to indicate the files are being deleted. */
+ update_ui_to_show_progress (self,
+ priv->n_deletions_pending,
+ priv->n_deletions_total);
+
+ /* Start all the deletion operations in parallel. They share the same
+ * #GCancellable. */
+ for (i = 0; i < files->len; i++)
+ {
+ GFile *file = files->pdata[i];
+
+ priv->n_deletions_to_start--;
+ g_file_delete_async (file, G_PRIORITY_DEFAULT, priv->delete_cancellable,
+ delete_files_cb, self);
+ }
+}
+
+static void
+delete_files_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObject *self;
+ MyObjectPrivate *priv;
+ GFile *file; /* unowned */
+ GError *error = NULL;
+
+ file = G_FILE (source_object);
+ self = MY_OBJECT (user_data);
+ priv = my_object_get_instance_private (self);
+
+ /* Finish deleting the file. */
+ g_file_delete_finish (file, result, &error);
+
+ if (error != NULL &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ {
+ g_warning ("Error deleting file: %s", error->message);
+ }
+
+ g_clear_error (&error);
+
+ /* Update the internal state. */
+ g_assert_cmpuint (priv->n_deletions_pending, >, 0);
+ priv->n_deletions_pending--;
+
+ /* Update the UI to show progress. */
+ update_ui_to_show_progress (self,
+ priv->n_deletions_pending,
+ priv->n_deletions_total);
+
+ /* If all deletions have completed, and no more are being started,
+ * update the UI to show completion. */
+ if (priv->n_deletions_pending == 0 && priv->n_deletions_to_start == 0)
+ {
+ update_ui_to_show_completion (self);
+
+ /* Clear the operation state. */
+ g_clear_object (&priv->delete_cancellable);
+ priv->n_deletions_total = 0;
+ }
+}
+
+static void
+my_object_dispose (GObject *obj)
+{
+ MyObjectPrivate *priv;
+
+ priv = my_object_get_instance_private (MY_OBJECT (obj));
+
+ /* Cancel any ongoing deletion operations.
+ *
+ * This ensures that if #MyObject is disposed part-way through the
+ * delete_files() set of operations, the set gets cancelled and
+ * doesn’t continue with an invalid #MyObject pointer. */
+ g_cancellable_cancel (priv->delete_cancellable);
+
+ /* Do other dispose calls here. */
+ …
+
+ /* Chain up. */
+ G_OBJECT_CLASS (my_object_parent_class)->dispose (obj);
+}</code>
+ </example>
+ </section>
+
+ <section id="gtask">
+ <title>Wrapping with <code>GTask</code></title>
+
+ <p>
+ Often when an asynchronous operation (or set of operations) becomes more
+ complex, it needs associated state. This is typically stored in a custom
+ structure — but defining a new structure to store the standard callback,
+ user data and cancellable tuple is laborious.
+ <link href="https://developer.gnome.org/gio/stable/GTask.html"><code>GTask</code></link>
+ eases this by providing a standardized way to wrap all three, plus extra
+ custom ‘task data’.
+ </p>
+
+ <p>
+ The use of a <code>GTask</code> can replace the use of a
+ <link
href="https://developer.gnome.org/gio/stable/GCancellable.html"><code>GCancellable</code></link>
+ for indicating whether an operation is ongoing.
+ </p>
+
+ <example>
+ <p>
+ This example is functionally the same as <link xref="#series"/>, but
+ refactored to use a <code>GTask</code> to wrap the sequence of
+ operations.
+ </p>
+
+ <p>
+ Key points in this example are:
+ </p>
+ <list>
+ <item><p>
+ State which was in <code>MyObjectPrivate</code> in
+ <link xref="#series"/> is now in the
+ <code>ConnectToServerData</code> closure, which is set as the ‘task
+ data’ of the <code>GTask</code> representing the overall operation.
+ This means it’s automatically freed after the operation returns.
+ </p></item>
+ <item><p>
+ Furthermore, this means that manipulations of
+ <code>MyObjectPrivate</code> state are limited to the start and end
+ of the sequence of operations, so reusing the task in different
+ situations becomes easier — for example, it is now a lot easier to
+ support running multiple such tasks in parallel.
+ </p></item>
+ <item><p>
+ As the <code>GTask</code> holds a reference to
+ <code>MyObject</code>, it is impossible for the object to be
+ disposed while the sequence of operations is ongoing, so the
+ <code>my_object_dispose()</code> code has been removed. Instead, a
+ <code>my_object_close()</code> method exists to allow any pending
+ operations can be cancelled so <code>MyObject</code> can be disposed
+ when desired.
+ </p></item>
+ </list>
+
+ <code mime="text/x-csrc" style="valid">
+static void
+connect_to_server_cb1 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void
+connect_to_server_cb2 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void
+connect_to_server_cb3 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+typedef struct {
+ GSocketConnection *connection; /* nullable; owned */
+ guint8 message_buffer[128];
+} ConnectToServerData;
+
+static void
+connect_to_server_data_free (ConnectToServerData *data)
+{
+ g_clear_object (&data->connection);
+}
+
+void
+my_object_connect_to_server_async (MyObject *self,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ MyObjectPrivate *priv;
+ GTask *task = NULL; /* owned */
+ ConnectToServerData *data = NULL; /* owned */
+ GFile *address_file = NULL; /* owned */
+
+ g_return_if_fail (MY_IS_OBJECT (self));
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ priv = my_object_get_instance_private (self);
+
+ if (priv->connect_task != NULL)
+ {
+ g_task_report_new_error (self, callback, user_data, NULL,
+ G_IO_ERROR, G_IO_ERROR_PENDING,
+ "Already connecting to the server.");
+ return;
+ }
+
+ /* Set up a cancellable. */
+ if (cancellable != NULL)
+ {
+ g_object_ref (cancellable);
+ }
+ else
+ {
+ cancellable = g_cancellable_new ();
+ }
+
+ /* Set up the task. */
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_check_cancellable (task, FALSE);
+
+ data = g_malloc0 (sizeof (ConnectToServerData));
+ g_task_set_task_data (task, data,
+ (GDestroyNotify) connect_to_server_data_free);
+
+ g_object_unref (cancellable);
+
+ priv->connect_task = g_object_ref (task);
+
+ /* Read the socket address. */
+ address_file = build_address_file ();
+ g_file_load_contents_async (address_file, g_task_get_cancellable (task),
+ connect_to_server_cb1, g_object_ref (task));
+ g_object_unref (address_file);
+
+ g_clear_object (&task);
+}
+
+static void
+connect_to_server_cb1 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObject *self;
+ MyObjectPrivate *priv;
+ GTask *task = NULL; /* owned */
+ GFile *address_file; /* unowned */
+ gchar *address = NULL; /* owned */
+ gsize address_size = 0;
+ GInetAddress *inet_address = NULL; /* owned */
+ GInetSocketAddress *inet_socket_address = NULL; /* owned */
+ guint16 port = 123;
+ GSocketClient *socket_client = NULL; /* owned */
+ GError *error = NULL;
+
+ address_file = G_FILE (source_object);
+ task = G_TASK (user_data);
+ self = g_task_get_source_object (task);
+ priv = my_object_get_instance_private (self);
+
+ /* Finish loading the address. */
+ g_file_load_contents_finish (address_file, result, &address,
+ &address_size, NULL, &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+ /* Parse the address. */
+ inet_address = g_inet_address_new_from_string (address);
+
+ if (inet_address == NULL)
+ {
+ /* Error. */
+ g_set_error (&error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ "Invalid address ‘%s’.", address);
+ goto done;
+ }
+
+ inet_socket_address = g_inet_socket_address_new (inet_address, port);
+
+ /* Connect to the given address. */
+ socket_client = g_socket_client_new ();
+
+ g_socket_client_connect_async (socket_client,
+ G_SOCKET_CONNECTABLE (inet_socket_address),
+ g_task_get_cancellable (task),
+ connect_to_server_cb2,
+ g_object_ref (task));
+
+done:
+ if (error != NULL)
+ {
+ /* Stop the operation and propagate the error. */
+ g_clear_object (&priv->connect_task);
+ g_task_return_error (task, error);
+ }
+
+ g_free (address);
+ g_clear_object (&inet_address);
+ g_clear_object (&inet_socket_address);
+ g_clear_object (&socket_client);
+ g_clear_object (&task);
+}
+
+static void
+connect_to_server_cb2 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObject *self;
+ MyObjectPrivate *priv;
+ GTask *task = NULL; /* owned */
+ ConnectToServerData *data; /* unowned */
+ GSocketClient *socket_client; /* unowned */
+ GSocketConnection *connection = NULL; /* owned */
+ GInputStream *input_stream; /* unowned */
+ GError *error = NULL;
+
+ socket_client = G_SOCKET_CLIENT (source_object);
+ task = G_TASK (user_data);
+ data = g_task_get_task_data (task);
+ self = g_task_get_source_object (task);
+ priv = my_object_get_instance_private (self);
+
+ /* Finish connecting to the socket. */
+ connection = g_socket_client_connect_finish (socket_client, result,
+ &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+ /* Store a reference to the connection so it is kept open while we read from
+ * it: #GInputStream does not keep a reference to a #GIOStream which contains
+ * it. */
+ data->connection = g_object_ref (connection);
+
+ /* Read a message from the connection. As the buffer is allocated as part of
+ * the per-task @data, multiple tasks can run concurrently. */
+ input_stream = g_io_stream_get_input_stream (G_IO_STREAM (connection));
+
+ g_input_stream_read_async (input_stream,
+ data->message_buffer,
+ sizeof (data->message_buffer),
+ G_PRIORITY_DEFAULT, g_task_get_cancellable (task),
+ connect_to_server_cb3, g_object_ref (task));
+
+done:
+ if (error != NULL)
+ {
+ /* Stop the operation and propagate the error. */
+ g_clear_object (&priv->connect_task);
+ g_task_return_error (task, error);
+ }
+
+ g_clear_object (&connection);
+ g_clear_object (&task);
+}
+
+static void
+connect_to_server_cb3 (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ MyObject *self;
+ MyObjectPrivate *priv;
+ GTask *task = NULL; /* owned */
+ ConnectToServerData *data; /* unowned */
+ GInputStream *input_stream; /* unowned */
+ gssize len = 0;
+ GError *error = NULL;
+
+ input_stream = G_INPUT_STREAM (source_object);
+ task = G_TASK (user_data);
+ data = g_task_get_task_data (task);
+ self = g_task_get_source_object (task);
+ priv = my_object_get_instance_private (self);
+
+ /* Finish reading from the socket. */
+ len = g_input_stream_read_finish (input_stream, result, &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+ /* Handle the message. */
+ g_assert_cmpint (len, >=, 0);
+ g_assert_cmpuint ((gsize) len, <=, sizeof (data->message_buffer));
+
+ handle_received_message (self, data->message_buffer, len, &error);
+
+ if (error != NULL)
+ {
+ goto done;
+ }
+
+ /* Success! */
+ g_task_return_boolean (task, TRUE);
+
+done:
+ /* Unconditionally mark the operation as finished.
+ *
+ * The streams should automatically close as this
+ * last reference is dropped. */
+ g_clear_object (&priv->connect_task);
+
+ if (error != NULL)
+ {
+ /* Stop the operation and propagate the error. */
+ g_task_return_error (task, error);
+ }
+
+ g_clear_object (&task);
+}
+
+void
+my_object_connect_to_server_finish (MyObject *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_if_fail (MY_IS_OBJECT (self));
+ g_return_if_fail (g_task_is_valid (result, self));
+ g_return_if_fail (error == NULL || *error == NULL);
+
+ g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+my_object_close (MyObject *self)
+{
+ MyObjectPrivate *priv;
+
+ g_return_if_fail (MY_IS_OBJECT (self));
+
+ priv = my_object_get_instance_private (self);
+
+ if (priv->connect_task != NULL)
+ {
+ GCancellable *cancellable = g_task_get_cancellable (priv->connect_task);
+ g_cancellable_cancel (cancellable);
+ }
+}</code>
+ </example>
+ </section>
+ </section>
+</page>
diff --git a/programming-guidelines/C/main-contexts.page b/programming-guidelines/C/main-contexts.page
index 2bc60afa..441bc0fa 100644
--- a/programming-guidelines/C/main-contexts.page
+++ b/programming-guidelines/C/main-contexts.page
@@ -466,14 +466,18 @@ do_computation (gpointer user_data)
</p>
<p>
- Always write things asynchronously internally (using
+ <link xref="async-programming">Write things asynchronously</link>
+ internally (using
<link xref="#gtask"><code>GTask</code></link> where appropriate), and keep
synchronous wrappers at the very top level of an API, where they can be
implemented by calling <code>g_main_context_iteration()</code> on a
specific <code>GMainContext</code>. Again, this makes future refactoring
easier. This is demonstrated in the above example: the thread uses
<code>g_output_stream_write_async()</code> rather than
- <code>g_output_stream_write()</code>.
+ <code>g_output_stream_write()</code>. A worker thread may be used instead,
+ and this can simplify the callback chain for long series of asynchronous
+ calls; but at the cost of increased complexity in verifying the code is
+ race-free.
</p>
<p>
diff --git a/programming-guidelines/C/threading.page b/programming-guidelines/C/threading.page
index 61168e58..2e9498ff 100644
--- a/programming-guidelines/C/threading.page
+++ b/programming-guidelines/C/threading.page
@@ -55,7 +55,7 @@
When writing projects using GLib, the default approach should be to
<em style="strong">never use threads</em>. Instead, make proper use of the
<link xref="main-contexts">GLib main context</link> which, through the use
- of asynchronous operations,
+ of <link xref="async-programming">asynchronous operations</link>,
allows most blocking I/O operations to continue in the background while
the main context continues to process other events. Analysis, review and
debugging of threaded code becomes very hard, very quickly.
diff --git a/programming-guidelines/Makefile.am b/programming-guidelines/Makefile.am
index 5b2bf330..affd1067 100644
--- a/programming-guidelines/Makefile.am
+++ b/programming-guidelines/Makefile.am
@@ -9,6 +9,7 @@ HELP_EXTRA = \
HELP_FILES = \
additional-materials.page \
api-stability.page \
+ async-programming.page \
c-coding-style.page \
cc-by-sa-3-0.xml \
databases.page \
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]