【问题标题】:Is GList (glib-doubly-linked-list) thread safe?GList(glib-doubly-linked-list)线程安全吗?
【发布时间】:2013-05-16 06:05:13
【问题描述】:

在一个多线程 C 程序中,我使用了 GLib (https://developer.gnome.org/glib/2.35/glib-Doubly-Linked-Lists.html#g-list-append) 的 GList 功能,其中多个线程创建了自己的列表。我观察到不可预知的崩溃,有时在应用程序加载时就发生了。堆栈跟踪显示了 glist_* 函数中的一些崩溃,一些消息如下:

(gdb) bt
#0  0x00007fffeb54a964 in g_slice_alloc () from /lib64/libglib-2.0.so.0
#1  0x00007fffeb52aac6 in g_list_append () from /lib64/libglib-2.0.so.0

或者这样的消息:

内存错误:[25628]:GSlice:断言失败:sys_page_size == 0 中止(核心转储)

(process:15426): GLib-ERROR (recursed) **: gmem.c:157: failed to allocate 137438953456 >bytes 中止... 中止(核心转储)

我有理由相信 GList 的引入导致了所有这些崩溃。在单线程程序中,我从未见过这些问题。

GList 本质上是线程安全的吗?如果没有,我需要做什么?

【问题讨论】:

    标签: c linux thread-safety glib


    【解决方案1】:

    你在使用 GThread 吗?:

    调用 g_thread_init() 后,GLib 是完全线程安全的。

    看看this page

    【讨论】:

    • 我没有,我使用了 POSIX 线程(pthread_create 等)。在生成子线程之前,我应该从父线程使用 g_thread_init(),并以 NULL 作为参数吗?
    • 不,您必须使用 g_thread 系列函数,另一方面,如果使用 POSIX 样式线程,则定义 G_THREADS_IMPL_POSIX
    • 继续阅读:“GLib 是完全线程安全的(所有全局数据都被自动锁定),但出于性能原因,单个数据结构实例不会自动锁定。因此,例如,您必须协调对相同的访问来自多个线程的 GHashTable。该规则的两个值得注意的例外是 GMainLoop 和 GAsyncQueue,它们是线程安全的,不需要进一步的应用程序级锁定即可从多个线程访问。您说线程创建它们自己的列表,但是它们是从多个线程同时访问的吗?
    • 您好 nemequ,感谢您的评论。在我的例子中,线程创建了自己的列表,并且只能从同一个线程访问,不会发生来自多个线程的并发访问。
    • 警告 g_thread_init 自 2.32 版以来已被弃用,不应在新编写的代码中使用。不再需要此功能。 GLib 线程系统在程序开始时自动初始化。抱歉,我正在阅读旧文档:(
    【解决方案2】:

    首先调试符号可能会有所帮助。有关如何获取它们的详细说明在on gnome live 中进行了描述,具体取决于您使用的发行版。

    如前所述,g_slice API 是线程安全的。 IIRC 它被设计为无锁或非常接近无锁。如前所述,GLib 数据结构通常可以安全地在多线程环境中使用(只要不同时从多个线程访问实例) - 如果不是,则应该报告 upstream 的错误(但是作为 GLib被广泛使用,包括多线程环境,它不太可能有一些明显的错误)。

    鉴于堆栈跟踪,它看起来像是内存损坏。我猜你在某处有缓冲区溢出/下溢并写入 g_slice 内部内存,或者你使用未初始化的 GList 指针,具有类似访问“随机内存”的效果,或者你尝试传递一个负值来分配由于某些原因导致溢出整数。我建议在Valgrind 下运行G_SLICE=always-malloc。如果速度太慢,还有其他方法,例如 gcc 4.8+ 中的 AddressSanitizer 和 clang(我不记得来自哪个版本)。请注意,此类错误可能不在与 GList 相关的代码中,而是由微妙的交互(不同的地址布局等)引起的。

    一旦你完成了它(使用调试符号并使用 valgrind 运行),你应该对正在发生的事情以及错误可能在哪里有更好的了解。请注意,它不会解决所有错误,但它会帮助解决最常见的情况。

    【讨论】:

    • GList 等简单的数据结构默认情况下是线程安全的。如果您查看源代码,您会发现没有并发保护。
    • @Ancurio:对不起。澄清 - 我的意思是 API 而不是实例(即在多线程环境中使用是安全的,不是线程安全的)。
    【解决方案3】:

    引用来自this page的文档:

    GLib 的一般策略是所有函数都是不可见的线程安全的,除了数据结构操作函数,如果你有两个线程操作同一个数据结构,它们必须使用锁来同步它们的操作。 GLib 为自己的目的创建一个工作线程,因此 GLib 应用程序将始终具有至少 2 个线程。

    【讨论】:

      【解决方案4】:

      与大多数其他简单的 GLib 数据结构一样,GList 不是线程安全的。但是,由于您没有同时从多个线程修改同一个列表,因此这应该没有问题。我认为实际上导致您的段错误的是对 g_slice_alloc 的多个并发调用(由 g_list_append/prepend 调用引起)。 same problem之前好像有其他人碰过。

      我解决此问题的方法是为每个分配您正在使用的内存的 g_list 一个受互斥保护的包装函数(我们以 g_list_append 为例),然后编写:

      /* Init this before starting your threads with g_mutex_new() */
      static GMutex *g_list_mutex;
      
      GList *safe_list_append(GList *list, gpointer data)
      {
          GList *ret;
      
          g_mutex_lock(g_list_mutex);
          ret = g_list_append(list, data);
          g_mutex_unlock(g_list_mutex);
      
          return ret;
      }
      

      (我没有测试这个)

      请注意,您需要在所有包装函数中使用相同的互斥锁,因此一次只有一个线程进入切片函数。

      【讨论】:

      • g_slice api 应该是线程安全的(参见 nemequ 注释,IIRC 它在内部是无锁的) - 如果它崩溃,那么 a) glib 中存在应该在上游报告的错误(不太可能考虑到广泛的在bugzilla.gnome.org 上使用多线程程序使用)或b)程序本身存在错误 - 因为G_SLICE=always-malloc 也出现了喜欢的问题,它更可能,否则这意味着但在两者中系统 malloc 和 slice。在您的解决方案中 - 您只保护单个调用,因此释放内存和追加将同时完成。
      • 因此,即使 slice API 和 malloc 出现问题,它也不会对其进行保护。
      • >“您只保护单个调用,因此释放内存和追加将同时完成” - 我不明白这一点,您能澄清一下吗?
      • 由编辑修复。然而,我关于在错误的地方解决错误问题的观点仍然成立。通常系统库(和 GLib 就是其中之一)是您应该寻找错误的最后一个地方,因为它们通常经过很好的测试(通过单元测试和广泛部署)。即使它们错误,也应该在上游报告它们,而不是在您的代码中解决问题,因为 a)您使您的代码比预期的要慢得多,并且 b)在上游修复将解决每个人的问题不只是你。
      • 是的,起初我只是考虑一个函数来编写示例,但后来我更新了它以防 OP 使用其他 g_list 调用。此外,如果这确实是上游问题,通常您仍然希望在此期间使用一种解决方法。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多