【问题标题】:Why does this recursive pthread_create call result in data race?为什么这个递归 pthread_create 调用会导致数据竞争?
【发布时间】:2015-09-14 08:59:54
【问题描述】:

当递归调用 pthread_create() 时,我遇到了数据竞争。 我不知道递归是否会导致问题, 但比赛似乎从未在第一次迭代中发生,主要是在第二次迭代中,很少发生在第三次迭代中。

使用 libgc 时,会出现内存损坏症状,例如分段错误,这与数据竞争相吻合。

以下程序是说明问题的最小示例。 我没有在示例中使用 libgc,因为只有数据竞争才是这个问题的主题。

使用 Helgrind 工具运行 Valgrind 时可以看到数据竞争。 报告的问题略有不同,有时甚至完全没有问题。

我正在运行 Linux Mint 17.2。 gcc的版本是(Ubuntu 4.8.4-2ubuntu1~14.04)4.8.4。

以下示例“main.c”重现了该问题。它遍历一个链表,在一个单独的线程中打印每个元素的值:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>


typedef struct List {
  int head ;
  struct List* tail ;
} List ;

// create a list element with an integer head and a tail
List* new_list( int head, List* tail ) {
  List* l = (List*)malloc( sizeof( List ) ) ;
  l->head = head ;
  l->tail = tail ;
  return l ;
}


// create a thread and start it
void call( void* (*start_routine)( void* arg ), void* arg ) {
  pthread_t* thread = (pthread_t*)malloc( sizeof( pthread_t ) ) ;

  if ( pthread_create( thread, NULL, start_routine, arg ) ) {
    exit( -1 ) ;
  }

  pthread_detach( *thread ) ;
  return ;
}


void print_list( List* l ) ;

// start routine for thread
void* print_list_start_routine( void* arg ) {

  // verify that the list is not empty ( = NULL )
  // print its head
  // print the rest of it in a new thread
  if ( arg ) {

    List* l = (List*)arg ;

    printf( "%d\n", l->head ) ;

    print_list( l->tail ) ;

  }

  return NULL ;
}

// print elements of a list with one thread for each element printed
// threads are created recursively
void print_list( List* l ) {
  call( print_list_start_routine, (void*)l ) ;
}


int main( int argc, const char* argv[] ) {

  List* l = new_list( 1, new_list( 2, new_list( 3, NULL ) ) ) ;

  print_list( l ) ;  

  // wait for all threads to finnish
  pthread_exit( NULL ) ;

  return 0 ;
}

这里是'makefile':

CC=gcc

a.out: main.o
    $(CC) -pthread main.o

main.o: main.c
    $(CC) -c -g -O0 -std=gnu99 -Wall main.c

clean:
    rm *.o a.out

这是 Helgrind 最常见的输出。请注意,只有一个数字 1、2 和 3 的行是程序的输出,而不是 Helgrind:

$ valgrind --tool=helgrind ./a.out 
==13438== Helgrind, a thread error detector
==13438== Copyright (C) 2007-2013, and GNU GPL'd, by OpenWorks LLP et al.
==13438== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==13438== Command: ./a.out
==13438== 
1
2
==13438== ---Thread-Announcement------------------------------------------
==13438== 
==13438== Thread #3 was created
==13438==    at 0x515543E: clone (clone.S:74)
==13438==    by 0x4E44199: do_clone.constprop.3 (createthread.c:75)
==13438==    by 0x4E458BA: pthread_create@@GLIBC_2.2.5 (createthread.c:245)
==13438==    by 0x4C30C90: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==13438==    by 0x4007EB: call (main.c:25)
==13438==    by 0x400871: print_list (main.c:58)
==13438==    by 0x40084D: print_list_start_routine (main.c:48)
==13438==    by 0x4C30E26: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==13438==    by 0x4E45181: start_thread (pthread_create.c:312)
==13438==    by 0x515547C: clone (clone.S:111)
==13438== 
==13438== ---Thread-Announcement------------------------------------------
==13438== 
==13438== Thread #2 was created
==13438==    at 0x515543E: clone (clone.S:74)
==13438==    by 0x4E44199: do_clone.constprop.3 (createthread.c:75)
==13438==    by 0x4E458BA: pthread_create@@GLIBC_2.2.5 (createthread.c:245)
==13438==    by 0x4C30C90: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==13438==    by 0x4007EB: call (main.c:25)
==13438==    by 0x400871: print_list (main.c:58)
==13438==    by 0x4008BB: main (main.c:66)
==13438== 
==13438== ----------------------------------------------------------------
==13438== 
==13438== Possible data race during write of size 1 at 0x602065F by thread #3
==13438== Locks held: none
==13438==    at 0x4C368F5: mempcpy (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==13438==    by 0x4012CD6: _dl_allocate_tls_init (dl-tls.c:436)
==13438==    by 0x4E45715: pthread_create@@GLIBC_2.2.5 (allocatestack.c:252)
==13438==    by 0x4C30C90: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==13438==    by 0x4007EB: call (main.c:25)
==13438==    by 0x400871: print_list (main.c:58)
==13438==    by 0x40084D: print_list_start_routine (main.c:48)
==13438==    by 0x4C30E26: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==13438==    by 0x4E45181: start_thread (pthread_create.c:312)
==13438==    by 0x515547C: clone (clone.S:111)
==13438== 
==13438== This conflicts with a previous read of size 1 by thread #2
==13438== Locks held: none
==13438==    at 0x51C10B1: res_thread_freeres (in /lib/x86_64-linux-gnu/libc-2.19.so)
==13438==    by 0x51C1061: __libc_thread_freeres (in /lib/x86_64-linux-gnu/libc-2.19.so)
==13438==    by 0x4E45199: start_thread (pthread_create.c:329)
==13438==    by 0x515547C: clone (clone.S:111)
==13438== 
3
==13438== 
==13438== For counts of detected and suppressed errors, rerun with: -v
==13438== Use --history-level=approx or =none to gain increased speed, at
==13438== the cost of reduced accuracy of conflicting-access information
==13438== ERROR SUMMARY: 8 errors from 1 contexts (suppressed: 56 from 48)

正如 Pooja Nilangekar 所述,将 pthread_detach() 替换为 pthread_join() 可以消除竞争。但是,分离线程是一项要求,因此目标是干净分离线程。换句话说,在移除比赛的同时保留 pthread_detach()。

线程之间似乎有一些意外的共享。 无意分享可能与此处讨论的内容有关:http://www.domaigne.com/blog/computing/joinable-and-detached-threads/ 尤其是示例中的错误。

我还是不明白到底发生了什么。

【问题讨论】:

  • 您是否尝试将-pthread 添加到makefile 的编译规则中?
  • 是的,但我删除了它。它没有效果。我认为只有链接阶段才有必要。

标签: c pthreads


【解决方案1】:

hlgrind 的输出与您的来源不匹配。根据 helgrind 的说法,在第 25 行有一个pthread_create 调用,但我看到的只是exit(-1)。我假设您忘记在源代码的开头添加一行。

话虽如此,我根本无法重现 hlgrind 的输出。我在while循环中运行了你的程序,希望得到同样的错误,但是nada。这就是比赛令人讨厌的地方——你永远不知道它们什么时候发生,而且很难追踪。

还有另一件事:每当解析器状态信息 (DNS) 将被释放时,都会调用 res_thread_freeres。实际上,它甚至没有被检查就被调用了。 _dl_allocate_tls_init 用于线程本地存储 (TLS) 并确保在您的函数控制线程之前分配/存储某些资源和元数据(自定义堆栈、清理信息等)。

这表明在创建新线程和杀死旧线程之间存在竞争。由于您分离了线程,因此父线程可能会在子线程完成之前死亡。在这种情况下,同步线程的退出(Pooja Nilangekar 指出这可以通过加入它们来完成)可能会解决问题,因为pthread_join 会停止直到线程完成,从而同步子/父释放。

如果您仍然想使用并行性,您可以做的就是自己处理内存。请参阅pthread_attr_setstack,具体请参见此处。由于我无法重现该错误,因此我不确定这是否真的有效。 此外,这种方法要求您知道您将拥有的线程数量。如果你试图重新分配线程当前使用的内存,你就是在玩火。

【讨论】:

  • 我删除了源代码中的一些行,以便不假思索地对其进行美化。据我所知,我没有使用 TLS。有没有办法验证这一点?另外,我认为没有使用 DNS。
  • 正如我刚才所说 - res_thread_freeres 调用已完成,检查您是否使用了 DNS 在该函数中。即使该检查将在功能之外 - 仍然需要进行检查。如果该检查是读取一个字节,则存在您的竞争条件。并且 TLS 在该实例中指的是即使对您的线程也基本上不可见的数据,因为线程分配中总是存在一些开销。但它与线程的整个堆栈帧存储在同一个内存块中。
  • 我认为 pthread_attr_setstack() 的解决方案是正确的方法。我还没试过。
【解决方案2】:

pthread_detach( *thread ) ; 行替换为pthread_join(*thread,NULL);。这将确保子进程在父进程之前终止,因此不会出现段错误。

【讨论】:

  • 这消除了数据竞争,但 pthread_join() 不会导致等待新创建的线程完成,实质上消除了并行性并使程序顺序化?
  • 您并没有删除并行性,您只是确保子级在父级之前终止,您仍在并行打印列表。所以它是并行执行和顺序终止。实际上,为了确保更好的并行性,我建议你重写你的函数。不要在call() 函数中创建pthread,而是在print_list_start_routine() 函数中创建它。
  • 我明白了。我会考虑的。这可能很棒!
  • 我想过。我需要使用分离。我发现了一些关于“堆栈回收”和我认为相关的 TCB。我明天会通读一遍。
【解决方案3】:

只是一个注释(我没有代表发表评论),我得到了非常相似的 helgrind 输出,没有递归。我使用 lambda 生成一个线程并将其分离。

==9060== Possible data race during write of size 1 at 0x126CE63F by thread #1
==9060== Locks held: none
==9060==    at 0x4C36D85: mempcpy (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==9060==    by 0x4012D66: _dl_allocate_tls_init (dl-tls.c:436)
==9060==    by 0x6B04715: get_cached_stack (allocatestack.c:252)
==9060==    by 0x6B04715: allocate_stack (allocatestack.c:501)
==9060==    by 0x6B04715: pthread_create@@GLIBC_2.2.5 (pthread_create.c:500)
==9060==    by 0x4C30E0D: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)
==9060==    by 0x6359D23: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)()) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.25)
==9060==    by 0x404075: thread<main()::<lambda()> > (thread:138)
==9060==    by 0x404075: main (test1.cpp:162)
==9060== 
==9060== This conflicts with a previous read of size 8 by thread #2
==9060== Locks held: none
==9060==    at 0x6E83931: res_thread_freeres (in /lib/x86_64-linux-gnu/libc-2.19.so)
==9060==    by 0x6E838E1: __libc_thread_freeres (in /lib/x86_64-linux-gnu/libc-2.19.so)
==9060==    by 0x6B0419B: start_thread (pthread_create.c:329)
==9060==    by 0x6E1803C: clone (clone.S:111)
==9060==  Address 0x126ce63f is not stack'd, malloc'd or on a free list

但是我在循环中这样做,我只被报告过一次。这表明 TLS 机制中可能存在触发警报的可能性。

【讨论】: