主要上下文

概要

  • 使用 g_main_context_invoke_full() 在其他线程中调用函数,假设每个线程都有一个线程默认的主上下文,该上下文在其整个生命周期中运行

  • 使用 GTask 在后台运行一个函数,而无需关心所使用的特定线程

  • 在编写代码时,大量使用断言来检查哪个上下文执行每个函数,并添加这些断言

  • 显式记录函数预计在哪个上下文中调用、回调将在哪个上下文中被调用,或信号将在哪个上下文中被发出

  • 小心 g_idle_add() 和类似函数,它们隐式使用全局默认主上下文

什么是 GMainContext?

GMainContext 是一个事件循环的通用实现,对于实现轮询文件 I/O 或基于事件的小部件系统(如 GTK)非常有用。它是几乎每个 GLib 应用程序的核心。要理解 GMainContext 需要理解 poll() 和轮询 I/O。

一个 GMainContext 拥有一组与其“附加”的源,每个源都可以被认为是一个期望的事件,并带有相关的回调函数,当该事件发生时将调用该函数;或者等效地,可以认为是一组需要检查的文件描述符 (FD)。一个事件可以是超时或在套接字上接收到的数据,例如。事件循环的一次迭代将

  • 准备源,确定是否有任何源可以立即分派。

  • 轮询源,阻塞当前线程,直到为其中一个源接收到事件。

  • 检查哪些源接收到事件(可能有多个)。

  • 从这些源分派回调。

这在 GLib 文档 中解释得非常好。

在其核心,GMainContext 只是一个 poll() 循环,循环的准备、检查和分派阶段对应于典型的 poll() 循环实现中的正常前导和后导部分。通常,在非平凡的 poll() 使用应用程序中,需要一些复杂性来跟踪正在轮询的 FD 列表。此外,GMainContext 添加了许多有用的功能,而普通的 poll() 不支持。最重要的是,它增加了线程安全性。

GMainContext 是完全线程安全的,这意味着可以在一个线程中创建 GSource,并将其附加到在另一个线程中运行的 GMainContext。这的一个典型用途是允许工作线程控制由中央 I/O 线程中的 GMainContext 正在侦听的套接字。每个 GMainContext 在每次迭代时被一个线程“获取”。其他线程不能在不获取它的情况下迭代一个 GMainContext,这保证了一个 GSource 及其 FD 将只由一个线程一次轮询(因为每个 GSource 最多附加到一个 GMainContext)。可以在迭代之间在线程之间交换 GMainContext,但这代价很高。

GMainContext 用于代替 poll(),主要是为了方便起见,因为它透明地处理动态管理传递给 poll() 的 FD 数组,尤其是在跨多个线程操作时。这是通过将文件描述符封装在 GSource 中来实现的,该 GSource 决定这些 FD 是否应该在主上下文迭代的“准备”阶段传递给 poll() 调用。

什么是 GMainLoop?

GMainLoop 本质上是以下几行代码,在删除引用计数和锁定后

loop->is_running = TRUE;
while (loop->is_running)
  {
    if (quit_condition)
      loop->is_running = FALSE;

    g_main_context_iteration (context, TRUE);
  }

quit_condition 设置为 TRUE 将导致循环在当前主上下文迭代结束后终止。

因此,GMainLoop 是一种方便、线程安全的方式来运行一个 GMainContext 以处理事件,直到满足所需的退出条件,此时应调用 g_main_loop_quit()。通常,在 UI 程序中,这将是用户单击“退出”。在套接字处理程序中,这可能是最终套接字关闭。

重要的是不要将主上下文与主循环混淆。主上下文完成大部分工作:准备源列表、等待事件和分派回调。主循环只是迭代一个上下文。

默认上下文

GMainContext 的一个重要特性是对“默认”上下文的支持。有两层默认上下文:线程默认和全局默认。全局默认(使用 g_main_context_default() 访问)由 GTK 在调用 g_application_run() 时运行。它也用于超时 (g_timeout_add()) 和空闲回调 (g_idle_add()) — 除非默认上下文正在运行,否则这些不会被分派!

线程默认上下文通常用于需要运行和分派回调的线程中的 I/O 操作。通过在启动 I/O 操作之前调用 g_main_context_push_thread_default(),可以设置线程默认上下文,并且 I/O 操作可以将它的源添加到该上下文。然后可以在 I/O 线程中的新主循环中运行该上下文,从而在 I/O 线程的堆栈上而不是在运行全局默认主上下文的线程的堆栈上分派回调。这允许 I/O 操作完全在单独的线程中运行,而无需显式地在任何地方传递特定的 GMainContext 指针。

相反,通过使用设置了特定线程默认上下文的开始一个长期运行的操作,调用代码可以保证该操作的回调将在该上下文中发出,即使该操作本身在工作线程中运行。这是 GTask 背后的原理:当创建一个新的 GTask 时,它会存储对当前线程默认上下文的引用,并在该上下文中分派其完成回调,即使任务本身使用 g_task_run_in_thread() 运行。

例如,下面的代码将运行一个 GTask,该任务将从一个线程并行执行两个写入操作。写入操作的回调将在工作线程中分派,而任务整体的回调将在 interesting_context 中分派。

typedef struct {
  GMainLoop *main_loop;
  guint n_remaining;
} WriteData;

/* This is always called in the same thread as thread_cb() because
 * it’s always dispatched in the @worker_context. */
static void
write_cb (GObject      *source_object,
          GAsyncResult *result,
          gpointer      user_data)
{
  WriteData *data = user_data;
  GOutputStream *stream = G_OUTPUT_STREAM (source_object);
  GError *error = NULL;
  gssize len;

  /* Finish the write. */
  len = g_output_stream_write_finish (stream, result, &error);
  if (error != NULL)
    {
      g_error ("Error: %s", error->message);
      g_error_free (error);
    }

  /* Check whether all parallel operations have finished. */
  write_data->n_remaining--;

  if (write_data->n_remaining == 0)
    {
      g_main_loop_quit (write_data->main_loop);
    }
}

/* This is called in a new thread. */
static void
thread_cb (GTask        *task,
           gpointer      source_object,
           gpointer      task_data,
           GCancellable *cancellable)
{
  /* These streams come from somewhere else in the program: */
  GOutputStream *output_stream1, *output_stream;
  GMainContext *worker_context;
  GBytes *data;
  const guint8 *buf;
  gsize len;

  /* Set up a worker context for the writes’ callbacks. */
  worker_context = g_main_context_new ();
  g_main_context_push_thread_default (worker_context);

  /* Set up the writes. */
  write_data.n_remaining = 2;
  write_data.main_loop = g_main_loop_new (worker_context, FALSE);

  data = g_task_get_task_data (task);
  buf = g_bytes_get_data (data, &len);

  g_output_stream_write_async (output_stream1, buf, len,
                               G_PRIORITY_DEFAULT, NULL, write_cb,
                               &write_data);
  g_output_stream_write_async (output_stream2, buf, len,
                               G_PRIORITY_DEFAULT, NULL, write_cb,
                               &write_data);

  /* Run the main loop until both writes have finished. */
  g_main_loop_run (write_data.main_loop);
  g_task_return_boolean (task, TRUE);  /* ignore errors */

  g_main_loop_unref (write_data.main_loop);

  g_main_context_pop_thread_default (worker_context);
  g_main_context_unref (worker_context);
}

/* This can be called from any thread. Its @callback will always be
 * dispatched in the thread which currently owns
 * @interesting_context. */
void
parallel_writes_async (GBytes              *data,
                       GMainContext        *interesting_context,
                       GCancellable        *cancellable,
                       GAsyncReadyCallback  callback,
                       gpointer             user_data)
{
  GTask *task;

  g_main_context_push_thread_default (interesting_context);

  task = g_task_new (NULL, cancellable, callback, user_data);
  g_task_set_task_data (task, data,
                        (GDestroyNotify) g_bytes_unref);
  g_task_run_in_thread (task, thread_cb);
  g_object_unref (task);

  g_main_context_pop_thread_default (interesting_context);
}

全局默认主上下文的隐式使用

几个函数隐式将源添加到全局默认主上下文。它们不应在多线程代码中使用。相反,请使用 g_source_attach() 以及下表中的替换函数创建的 GSource

全局默认主上下文的隐式使用意味着回调函数在主线程中调用,通常导致工作从工作线程带回到主线程。

不要使用

而是使用

g_timeout_add()

g_timeout_source_new()

g_idle_add()

g_idle_source_new()

g_child_watch_add()

g_child_watch_source_new()

因此,要在工作线程中延迟一些计算,请使用以下代码

static guint
schedule_computation (guint delay_seconds)
{
  /* Get the calling context. */
  GMainContext *context = g_main_context_get_thread_default ();

  GSource *source = g_timeout_source_new_seconds (delay_seconds);
  g_source_set_callback (source, do_computation, NULL, NULL);

  guint id = g_source_attach (source, context);

  g_source_unref (source);

  /* The ID can be used with the same @context to
   * cancel the scheduled computation if needed. */
  return id;
}

static void
do_computation (gpointer user_data)
{
  // ...
}

在库中使用 GMainContext

从高层来看,库代码不得对主上下文进行更改,这些更改可能会影响使用该库的应用程序的执行,例如通过更改应用程序源的分派时间。可以遵循各种最佳实践来帮助实现这一点。

切勿迭代库外部创建的上下文,包括全局默认或线程默认上下文。否则,应用程序中创建的源可能会在应用程序未预期时分派,从而导致应用程序代码出现重入问题。

在删除库对上下文的最后一个引用之前,始终从主上下文删除源,尤其是在它可能暴露给应用程序的情况下(例如,作为线程默认值)。否则,应用程序可能会保留对主上下文的引用,并在库返回后继续迭代它,从而可能导致库中意外源的分派。这相当于不假设删除库对主上下文的最后一个引用将最终确定该上下文。

如果库设计为从多个线程或以感知上下文的方式使用,则始终记录每个回调将在哪个上下文中分派。例如,“回调始终将在对象构造时线程默认的上下文中分派”。使用库 API 的开发人员需要知道此信息。

使用 g_main_context_invoke() 确保回调在正确的上下文中分派。这比手动使用 g_idle_source_new() 在上下文中传输工作更容易。

库绝不应使用 g_main_context_default()(或者,等效地,将 NULL 传递给 GMainContext 类型的参数)。始终存储并显式使用特定的 GMainContext,即使它通常指向一些默认上下文。这使得代码更容易在将来拆分为线程,而不会导致难以调试的问题,这些问题是由回调在错误的上下文中调用引起的。

在内部异步地进行操作(在适当的情况下使用 GTask),并在 API 的最顶层保留同步包装器,在那里可以通过在特定的 GMainContext 上调用 g_main_context_iteration() 来实现它们。同样,这使得未来的重构更容易。这在前面的示例中得到了演示:线程使用 g_output_stream_write_async() 而不是 g_output_stream_write()。可以使用工作线程,这可以简化长时间异步调用的回调链;但代价是增加了验证代码是否无竞争状态的复杂性。

始终匹配线程默认主上下文的推送和弹出:g_main_context_push_thread_default()g_main_context_pop_thread_default()

确保函数在正确的上下文中调用

“正确的上下文”是函数应该在其中执行的线程的线程默认主上下文。这假设典型的每个线程都有单个主上下文在主循环中运行的情况。主上下文有效地为线程提供了一个工作或消息队列——线程可以定期检查它以确定是否有来自另一个线程的工作待处理。将消息放在此队列上——在另一个主上下文中调用函数——将最终导致它在该线程的堆栈上分派。

例如,如果应用程序执行漫长且 CPU 密集型的计算,它应该在后台线程中安排此计算,以便主线程中的 UI 更新不会被阻止。但是,计算的结果可能需要在 UI 中显示,因此一旦计算完成,就必须在主线程中调用一些 UI 更新函数。

此外,如果计算函数可以限制为单个线程,那么很容易消除锁定其访问的大量数据的需要。这假设其他线程也以类似的方式实现,因此大多数数据仅由单个线程访问,线程通过消息传递进行通信。这允许每个线程随意更新其数据,从而大大简化了锁定。

对于某些函数,可能没有理由关心它们在哪个上下文中执行,也许因为它们是异步的,因此不会阻塞上下文。但是,仍然建议明确使用哪个上下文,因为这些函数可能会发出信号或调用回调,并且出于线程安全性的考虑,有必要知道将调用这些信号处理程序或回调的线程。

例如,g_file_copy_async() 中的进度回调记录为在初始调用时线程默认的上下文中调用。

调用原则

调用函数的特定上下文的核心原则很简单,并在下面进行介绍以解释这些概念。在实践中,应使用便利方法 g_main_context_invoke_full()

必须将 GSource 添加到目标 GMainContext,当它被分派时将调用该函数。此 GSource 几乎总是使用 g_idle_source_new() 创建的空闲源,但这不必如此。它可以是超时源,以便在延迟后执行该函数,例如。

一旦准备好,GSource 就会被分派,并在线程的堆栈上调用该函数。对于空闲源,这将是在分派优先级较高的所有源之后——可以使用空闲源的优先级参数通过 g_source_set_priority() 进行调整。该源通常然后被销毁,以便该函数仅执行一次(尽管,这不必如此)。

可以通过传递给 GSource 回调的 user_data 作为线程之间传递数据。这使用 g_source_set_callback() 设置源,以及要调用的回调函数。仅提供一个指针,因此如果需要传递多个数据字段,则必须将它们包装在分配的结构中。

下面的例子演示了底层原理,但下面解释的便利方法可以简化操作。

/* Main function for the background thread, thread1. */
static gpointer
thread1_main (gpointer user_data)
{
  GMainContext *thread1_main_context = user_data;
  GMainLoop *main_loop;

  /* Set up the thread’s context and run it forever. */
  g_main_context_push_thread_default (thread1_main_context);

  main_loop = g_main_loop_new (thread1_main_context, FALSE);
  g_main_loop_run (main_loop);
  g_main_loop_unref (main_loop);

  g_main_context_pop_thread_default (thread1_main_context);
  g_main_context_unref (thread1_main_context);

  return NULL;
}

/* A data closure structure to carry multiple variables between
 * threads. */
typedef struct {
  gchar   *some_string;  /* owned */
  guint    some_int;
  GObject *some_object;  /* owned */
} MyFuncData;

static void
my_func_data_free (MyFuncData *data)
{
  g_free (data->some_string);
  g_clear_object (&data->some_object);
  g_free (data);
}

static void
my_func (const gchar *some_string,
         guint        some_int,
         GObject     *some_object)
{
  /* Do something long and CPU intensive! */
}

/* Convert an idle callback into a call to my_func(). */
static gboolean
my_func_idle (gpointer user_data)
{
  MyFuncData *data = user_data;

  my_func (data->some_string, data->some_int, data->some_object);

  return G_SOURCE_REMOVE;
}

/* Function to be called in the main thread to schedule a call to
 * my_func() in thread1, passing the given parameters along. */
static void
invoke_my_func (GMainContext *thread1_main_context,
                const gchar  *some_string,
                guint         some_int,
                GObject      *some_object)
{
  GSource *idle_source;
  MyFuncData *data;

  /* Create a data closure to pass all the desired variables
   * between threads. */
  data = g_new0 (MyFuncData, 1);
  data->some_string = g_strdup (some_string);
  data->some_int = some_int;
  data->some_object = g_object_ref (some_object);

  /* Create a new idle source, set my_func() as the callback with
   * some data to be passed between threads, bump up the priority
   * and schedule it by attaching it to thread1’s context. */
  idle_source = g_idle_source_new ();
  g_source_set_callback (idle_source, my_func_idle, data,
                         (GDestroyNotify) my_func_data_free);
  g_source_set_priority (idle_source, G_PRIORITY_DEFAULT);
  g_source_attach (idle_source, thread1_main_context);
  g_source_unref (idle_source);
}

/* Main function for the main thread. */
static void
main (void)
{
  GThread *thread1;
  GMainContext *thread1_main_context;

  /* Spawn a background thread and pass it a reference to its
   * GMainContext. Retain a reference for use in this thread
   * too. */
  thread1_main_context = g_main_context_new ();
  g_thread_new ("thread1", thread1_main,
                g_main_context_ref (thread1_main_context));

  /* Maybe set up your UI here, for example. */

  /* Invoke my_func() in the other thread. */
  invoke_my_func (thread1_main_context,
                  "some data which needs passing between threads",
                  123456, some_object);

  /* Continue doing other work. */
}

此调用是单向的:它在 thread1 中调用 my_func(),但没有办法将值返回到主线程。要做到这一点,需要再次使用相同的原理,在主线程中调用回调函数。这是一个直接的扩展,此处未涵盖。

为了维护线程安全,可能被多个线程访问的数据必须使用互斥锁来使这些访问互斥。可能被多个线程访问的数据:thread1_main_context,传递到 thread1_main 的 fork 调用中;以及 some_object,其引用在数据闭包中传递。关键是,GLib 保证 GMainContext 是线程安全的,因此在线程之间共享 thread1_main_context 是安全的。该示例假定访问 some_object 的其他代码是线程安全的。

请注意,some_stringsome_int 无法从两个线程访问,因为它们的副本被传递到 thread1,而不是原始值。这是使跨线程调用线程安全而无需锁定的标准技术。它还避免了同步释放 some_string 的问题。

类似地,将对 some_object 的引用传递到 thread1,这可以解决对象销毁同步的问题。

g_idle_source_new() 用于代替更简单的 g_idle_add(),以便可以指定要附加的 GMainContext

便利方法:g_main_context_invoke_full()

这个方法通过便利方法 g_main_context_invoke_full() 得到了极大的简化。它调用一个回调函数,以便在调用期间拥有指定的 GMainContext。拥有主上下文几乎总是等同于运行它,因此该函数将在指定上下文是线程默认值的线程中调用。

如果用户数据不需要在调用返回后由 GDestroyNotify 回调函数释放,则可以使用 g_main_context_invoke()

修改前面的示例,可以将 invoke_my_func() 函数替换为以下内容

static void
invoke_my_func (GMainContext *thread1_main_context,
                const gchar  *some_string,
                guint         some_int,
                GObject      *some_object)
{
  MyFuncData *data;

  /* Create a data closure to pass all the desired variables
   * between threads. */
  data = g_new0 (MyFuncData, 1);
  data->some_string = g_strdup (some_string);
  data->some_int = some_int;
  data->some_object = g_object_ref (some_object);

  /* Invoke the function. */
  g_main_context_invoke_full (thread1_main_context,
                              G_PRIORITY_DEFAULT, my_func_idle,
                              data,
                              (GDestroyNotify) my_func_data_free);
}

考虑一下,如果 invoke_my_func()thread1 调用,而不是从主线程调用会发生什么。使用原始实现,空闲源将被添加到 thread1 的上下文并在此上下文的下一次迭代中分派(假设没有优先级更高的挂起分派)。使用改进后的实现,g_main_context_invoke_full() 将注意到指定的上下文已经由线程拥有(或者可以被它获取所有权),并将直接调用 my_func_idle(),而不是将源附加到上下文并延迟到下一次上下文迭代进行调用。

这种细微的行为差异在大多数情况下无关紧要,但值得记住,因为它会影响阻塞行为(invoke_my_func() 将从花费可忽略的时间,变为花费与 my_func() 相同的时间后返回)。

检查线程

以断言的形式记录每个函数应该在哪个线程中调用,这很有用

g_assert (g_main_context_is_owner (expected_main_context));

如果将这些放在每个函数的顶部,任何断言失败都会突出显示一个函数从错误线程调用的情况。在最初开发代码时编写这些断言比调试由于函数在错误线程中调用而可能轻松导致的竞争条件要容易得多。

这种技术也可以应用于信号发射和回调,从而提高类型安全性,并断言使用正确的上下文。请注意,通过 g_signal_emit() 发出的信号是同步的,并且根本不涉及主上下文。

例如,在发射信号时,不要使用以下方法

guint param1;  /* arbitrary example parameters */
gchar *param2;
guint retval = 0;

g_signal_emit_by_name (my_object,
                       "some-signal",
                       param1,
                       param2,
                       &retval);

可以使用以下方法

static guint
emit_some_signal (GObject     *my_object,
                  guint        param1,
                  const gchar *param2)
{
  guint retval = 0;

  g_assert (g_main_context_is_owner (expected_main_context));

  g_signal_emit_by_name (my_object, "some-signal",
                         param1, param2, &retval);

  return retval;
}

GTask

GTask 提供了一种不同的方法来在其他线程中调用函数,更适合于应在某个后台线程中执行函数,但不是特定线程的情况。

GTask 接受一个数据闭包和一个要执行的函数,并提供返回该函数结果的方法。它处理运行该函数所需的任何操作,该函数位于 GLib 内部的某个线程池中的任意线程中。

通过结合 g_main_context_invoke_full()GTask,可以在特定上下文中运行任务并毫不费力地将结果返回到当前上下文

/* This will be invoked in thread1. */
static gboolean
my_func_idle (gpointer user_data)
{
  GTask *task = G_TASK (user_data);
  MyFuncData *data;
  gboolean retval;

  /* Call my_func() and propagate its returned boolean to
   * the main thread. */
  data = g_task_get_task_data (task);
  retval = my_func (data->some_string, data->some_int,
                    data->some_object);
  g_task_return_boolean (task, retval);

  return G_SOURCE_REMOVE;
}

/* Whichever thread this is invoked in, the @callback will be
 * invoked in, once my_func() has finished and returned a result. */
static void
invoke_my_func_with_result (GMainContext        *thread1_main_context,
                            const gchar         *some_string,
                            guint                some_int,
                            GObject             *some_object,
                            GAsyncReadyCallback  callback,
                            gpointer             user_data)
{
  MyFuncData *data;

  /* Create a data closure to pass all the desired variables
   * between threads. */
  data = g_new0 (MyFuncData, 1);
  data->some_string = g_strdup (some_string);
  data->some_int = some_int;
  data->some_object = g_object_ref (some_object);

  /* Create a GTask to handle returning the result to the current
   * thread-default main context. */
  task = g_task_new (NULL, NULL, callback, user_data);
  g_task_set_task_data (task, data,
                        (GDestroyNotify) my_func_data_free);

  /* Invoke the function. */
  g_main_context_invoke_full (thread1_main_context,
                              G_PRIORITY_DEFAULT, my_func_idle,
                              task,
                              (GDestroyNotify) g_object_unref);
}