线程

概要

  • 尽可能不要使用线程。

  • 如果必须使用线程,请使用 GTaskGThreadPool,并尽可能地隔离线程代码。

  • 如果手动使用 GThread,请使用 g_thread_join() 以避免泄漏线程资源。

  • 在使用线程时,请注意代码执行的 GMainContext。在错误的上下文中执行代码可能会导致竞争条件或阻塞主循环。

何时使用线程

在使用 GLib 的项目中,默认方法是不使用线程。相反,请充分利用 GLib 主上下文,它通过使用异步操作,允许大多数阻塞 I/O 操作在后台继续进行,而主上下文继续处理其他事件。线程代码的分析、审查和调试会变得非常困难,而且速度很快。

只有在使用具有阻塞函数的外部库,并且需要从 GLib 代码调用这些函数时,才需要使用线程。如果库提供非阻塞替代方案,或者与 poll() 循环集成的方案,则应优先使用。如果确实必须使用阻塞函数,则应为其编写一个薄包装器,将其转换为 GLib 异步函数的正常 GAsyncResult 风格,并在工作线程中运行阻塞操作。

例如,以下阻塞函数

int
some_blocking_function (void *param1,
                        void *param2);

应由这对函数包装

void
some_blocking_function_async (void                 *param1,
                              void                 *param2,
                              GCancellable         *cancellable,
                              GAsyncReadyCallback   callback,
                              gpointer              user_data);

int
some_blocking_function_finish (GAsyncResult        *result,
                               GError             **error);

其实现如下所示

/* Closure for the call’s parameters. */
typedef struct {
  void *param1;
  void *param2;
} SomeBlockingFunctionData;

static void
some_blocking_function_data_free (SomeBlockingFunctionData *data)
{
  free_param (data->param1);
  free_param (data->param2);

  g_free (data);
}

static void
some_blocking_function_thread_cb (GTask         *task,
                                  gpointer       source_object,
                                  gpointer       task_data,
                                  GCancellable  *cancellable)
{
  SomeBlockingFunctionData *data = task_data;
  int retval;

  /* Handle cancellation. */
  if (g_task_return_error_if_cancelled (task))
    {
      return;
    }

  /* Run the blocking function. */
  retval = some_blocking_function (data->param1, data->param2);
  g_task_return_int (task, retval);
}

void
some_blocking_function_async (void                 *param1,
                              void                 *param2,
                              GCancellable         *cancellable,
                              GAsyncReadyCallback   callback,
                              gpointer              user_data)
{
  GTask *task = NULL;  /* owned */
  SomeBlockingFunctionData *data = NULL;  /* owned */

  g_return_if_fail (validate_param (param1));
  g_return_if_fail (validate_param (param2));
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (NULL, cancellable, callback, user_data);
  g_task_set_source_tag (task, some_blocking_function_async);

  /* Cancellation should be handled manually using mechanisms specific to
   * some_blocking_function(). */
  g_task_set_return_on_cancel (task, FALSE);

  /* Set up a closure containing the call’s parameters. Copy them to avoid
   * locking issues between the calling thread and the worker thread. */
  data = g_new0 (SomeBlockingFunctionData, 1);
  data->param1 = copy_param (param1);
  data->param2 = copy_param (param2);

  g_task_set_task_data (task, data, some_blocking_function_data_free);

  /* 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. */
  g_task_run_in_thread (task, some_blocking_function_thread_cb);

  g_object_unref (task);
}

int
some_blocking_function_finish (GAsyncResult  *result,
                               GError       **error)
{
  g_return_val_if_fail (g_task_is_valid (result,
                                         some_blocking_function_async), -1);
  g_return_val_if_fail (error == NULL || *error == NULL, -1);

  return g_task_propagate_int (G_TASK (result), error);
}

有关更多详细信息,请参阅 GTask 文档

使用线程

如果 GTask 不适合编写工作线程,则必须使用更底层的方案。应仔细考虑这一点,因为编写错误的线程代码很容易导致运行时出现无法预测的错误、死锁或消耗过多资源而终止程序。

编写线程代码的完整手册超出了本文档的范围,但以下是一些应遵循的指南,可以减少线程代码中出现错误的潜在可能性。首要原则是减少受线程影响的代码和数据量——例如,减少线程数量、工作线程实现的复杂性以及线程之间共享的数据量。

  • 如果可能,请使用 GThreadPool 代替手动创建线程。 GThreadPool 支持工作队列、已生成线程数量的限制,并自动连接完成的线程,因此不会发生泄漏。

  • 如果无法使用 GThreadPool(这种情况很少发生)

    • 请使用 g_thread_try_new() 来生成线程,而不是 g_thread_new(),以便可以优雅地处理系统耗尽线程的错误,而不是无条件地中止程序。

    • 使用 g_thread_join() 显式连接线程,以避免泄漏线程资源。

  • 使用消息传递在线程之间传输数据,而不是使用互斥锁进行手动锁定。 GThreadPool 显式支持这一点,方法是使用 g_thread_pool_push()

  • 如果必须使用互斥锁

    • 尽可能隔离线程代码,将互斥锁保持在类内部,并紧密绑定到特定的类成员。

    • 所有互斥锁都应在其声明旁边进行明确注释,指示它们保护哪些其他结构或变量的访问。同样,这些变量也应进行注释,说明只有在持有该互斥锁时才能访问它们。

  • 请注意主上下文和线程之间的交互。例如,g_timeout_add_seconds() 将超时添加到在全局默认主上下文中执行,该上下文由主线程运行,不一定是当前线程。搞错这一点可能意味着本应在工作线程中执行的工作最终在主线程中执行。

调试

调试线程问题很棘手,既因为它们很难重现,又因为它们很难推理。这是避免使用线程的主要原因之一。

但是,如果出现线程问题,Valgrind 的 drdhelgrind 工具很有用。