【问题标题】:Segmentation fault for pthreads in a recursive call递归调用中 pthread 的分段错误
【发布时间】:2011-10-03 07:14:48
【问题描述】:

鉴于下面的代码,如果我使用 n>16 运行它,我会遇到分段错误。

我认为它与堆栈有关,但我无法弄清楚。谁能帮我一把?代码不是我的,而且真的不重要。我只想有人帮我处理正在发生的事情。 This SO question 非常相似,但没有足够的信息(发布答案的人简短地谈到了问题,然后继续谈论不同的语言)。此外,请注意,使用两个 gig 并且没有递归,我可以(如果我做得对的话)成功地创建了 16000 多个线程(尽管操作系统只创建了大约 500 个并运行了大约 300 个)。无论如何,我在哪里得到段错误,为什么?谢谢。

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

static void* fibonacci_thread( void* arg ) {
int n = (int)arg, fib;

pthread_t th1, th2;

void* pvalue; /*Holds the value*/

switch (n) {
case 0:  return (void*)0;
case 1:  /* Fallthru, Fib(1)=Fib(2)=1 */
case 2:  return (void*)1;
default: break;
}

pthread_create(&th1, NULL, fibonacci_thread, (void*)(n-1));

pthread_create( &th2, NULL, fibonacci_thread, (void*)(n-2));

pthread_join(th1, &pvalue);

fib = (int)pvalue;

pthread_join(th2, &pvalue);

fib += (int)pvalue;

return (void*)fib;
}

int main(int argc, char *argv[])
{
int n=15;
printf ("%d\n",(int)fibonacci_thread((void*)n));
return 0;
}

【问题讨论】:

  • 首先检查pthread_createpthread_join的返回值。 (只要assert,如果你愿意,他们会返回零。)
  • 另外,这是在 32 位 Linux 系统上吗?我认为 pthreads 为每个线程分配了大约 2 兆的堆栈。这只是虚拟内存,不是物理内存,但它仍然会将您限制在大约 2000 个线程左右,然后才会出现问题。

标签: c stack pthreads segmentation-fault


【解决方案1】:

不是做斐波那契数列的好方法:-)

您的第一个线程启动另外两个线程,每个线程启动另外两个线程,依此类推。因此,当n &gt; 16 时,您最终可能会得到大量线程(数千个)(a)

除非您的 CPU 的内核比我的多方式,否则您将浪费时间为这样的 CPU 密集型任务运行数千个线程。对于纯粹受 CPU 限制的任务,最好拥有与可用的物理执行引擎(内核或 CPU)一样多的线程。显然,这会改变您不是纯粹受 CPU 限制的地方。


如果你想要一个高效的递归(非线程)斐波那契计算器,你应该使用类似(伪代码)的东西:

def fib(n, n ranges from 1 to infinity):
    if n is 1 or 2:
        return 1
    return fib(n-1) + fib(n-2)

斐波那契对于非线程递归来说甚至不是那么好,因为问题并没有很快减少。我的意思是计算fib(1000) 将使用1000 个堆栈帧。将此与只需要十个堆栈帧的递归二叉树搜索进行比较。这是因为前者只为每个栈帧移除了 1/1000 的搜索空间,而后者则移除了剩余搜索空间的一半。

做斐波那契的最好方法是迭代:

def fib(n, n ranges from 1 to infinity):
    if n is 1 or 2:
        return 1
    last2 = 1, last1 = 1
    for i ranges from 3 to n:
        last0 = last2 + last1
        last2 = last1
        last1 = last0
    return last0

当然,如果你想要一个令人眼花缭乱的快速斐波那契生成器,你可以编写一个程序来生成所有你可以存储的东西(例如,一个长值)并写出一个 C 结构包含它们。然后将该输出合并到您的 C 程序中,您的运行时“计算”将把任何其他方法排除在外。这是您标准的“以空间换时间”的优化方法:

long fib (size_t n) {
    static long fibs[] = {0, 1, 1, 2, 3, 5, 8, 13, ...};
    if (n > sizeof(fibs) / sizeof(*fibs))
        return -1;
    return fibs[n];
}

这些准则适用于搜索空间不会快速减少的大多数情况(不仅仅是斐波那契)。


(a) 最初,我认为这将是 216,但是,正如以下程序所示(感谢 Nemo 让我直截了当),它并不完全太糟糕了 - 当你接近 fib(0) 时,我没有考虑到生成线程的减少性质:

#include <stdio.h>
static count = 0;
static void fib(int n) {
        if (n <= 2) return;
        count++; fib(n-1);
        count++; fib(n-2);
}
int main (int argc, char *argv[]) {
        fib (atoi (argv[1]));
        printf ("%d\n", count);
        return 0;
}

这等效于您的代码,但它只是为每个生成的线程增加一个计数器,而不是实际生成它们。各种输入值的线程数为:

 N    Threads
---  ---------
  1          0
  2          0
  3          2
  4          4
  5          8
  6         14
  :
 14
 15      1,218
 16      1,972
  :
 20     13,528
  :
 26    242,784
  :
 32  4,356,616

现在请注意,虽然我说它没有 那么 很糟糕,但我并没有说它很好 :-) 即使 2000 个线程在一个系统上也是一个公平的负载,每个线程都有自己的自己的内核结构和堆栈。你可以看到,虽然增长开始很小,但它们迅速加速到无法控制的地步。而且第 32nd 个数字并不大——它只是超过 200 万的一个小数目。

所以底线仍然存在:在有意义的地方使用递归(您可以相对较快地减少搜索空间以免耗尽堆栈空间),并在有意义的地方使用 threds(在您不需要的地方)最终运行如此之多,以至于您使操作系统的资源过载)。

【讨论】:

  • “这不是做斐波那契数列的好方法”...告诉我...为什么像并行性这样的领域会使用斐波那契作为他们的主力示例?谢谢你。我是否应该能够获得/proc/sys/kernel/thread_max 线程?
  • @Dervin:在 glibc 荒谬的默认堆栈大小为 8 兆的 32 位系统上不适用。只有几百个线程,您将耗尽虚拟地址空间......最好使用pthread_attr_setstacksize
  • :我不认为这里的目标是计算斐波那契;似乎是最大限度地利用机器上的线程数......@R .. ooo good catch.
  • 2^16 绝对是错误的。请参阅我的答案中的第二个编辑以获取解释。
【解决方案2】:

见鬼,不妨把这个作为答案。

首先,检查pthread_createpthread_join的返回值。 (始终、始终、始终检查错误。如果您感到懒惰,只需 assert 它们返回零,但永远不要忽略它们。)

其次,我可以发誓 Linux glibc 默认为每个线程分配 2 兆字节的堆栈(可通过 pthread_attr_setstacksize 配置)。当然,这只是虚拟内存,但在 32 位系统上仍将您的线程总数限制为约 2000 个。

最后,我相信对这将产生的线程数的正确估计基本上是fib(n) 本身(多么好的递归)。或者大致是phi^n,其中phi(1+sqrt(5))/2。所以这里的线程数更接近 2000 而不是 65000,这与我对 32 位系统将用尽 VM 的位置的估计一致。

[编辑]

要确定系统上新线程的默认堆栈大小,请运行以下程序:

int main(int argc, char *argv[])
{
    size_t stacksize;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_getstacksize(&attr, &stacksize);
    phthread_attr_destroy(&attr);
    printf("Default stack size = %zd\n", stacksize);
    return 0;
}

[编辑 2]

重复一遍:这远不及 2^16 个线程。

设 f(n) 为计算 fib(n) 时产生的线程数。

当 n=16 时,一个线程生成两个新线程:一个用于计算 fib(15),另一个用于计算 fib(14)。所以 f(16) = f(15) + f(14) + 1。

通常 f(n) = f(n-1) + f(n-2) + 1。

事实证明,这种递归的解决方案是 f(n) 只是前 n 个斐波那契数的总和:

      1 + 1 + 2 + 3 + 5 + 8   // f(6)
+         1 + 1 + 2 + 3 + 5   // + f(5)
+ 1                           // + 1

= 1 + 1 + 2 + 3 + 5 + 8 + 13  // = f(7)

这(非常)大致是phi^(n+1),而不是2^n。 f(16) 的总数仍以千计,而不是数万计。

[编辑 3]

啊,我明白了,你的问题的症结是这个(从 cmets 中提升):

感谢 Nemo 的详细解答。我做了一个小测试 pthread_created ~10,000 个线程,其中只有一个 while(1) 循环,所以 他们没有终止……它确实终止了!确实,操作系统很聪明 只创建大约 1000 个并运行一个更小的数字,但它 没有用完堆栈。为什么生成时没有段错误 比 THREAD_MAX 多很多,但是当我递归执行时我会这样做?

这是我的猜测。

你只有几个核心。在任何时候,内核都必须决定 哪些 线程将运行。如果您有(比如说)2 个内核和 500 个线程,那么任何特定线程只会运行 1/250 的时间。因此,产生新线程的主循环不会经常运行。我什至不确定内核的调度程序对于单个进程中的线程是否“公平”,因此至少可以想象,如果有 1000 个线程,主线程根本就不会运行。

至少,每个执行while (1); 的线程将在放弃其时间片之前在其核心上运行1/HZ。这可能是 1 毫秒,但根据内核的配置方式,它可能高达 10 毫秒。因此,即使调度程序是公平的,当您拥有数千个线程时,您的主线程也只会每秒运行一次。

由于只有主线程在创建新线程,因此线程创建速度会慢到爬行甚至可能停止。

试试这个。尝试使用while (1) pause();,而不是while (1); 作为实验中的子线程。 (pause 来自 unistd.h。)这将使子线程保持阻塞,并应允许主线程继续创建新线程,从而导致崩溃。

再一次,请检查pthread_create 返回的内容。

【讨论】:

  • 我认为你的线程计算偏低:fib(16) 产生 fib(15) 和 fib(14) 然后等待。 fib(15) 生成 fib(14) 和 fib(13) 然后等待,依此类推。这是每个递归级别的两倍,2^16 是 64K。即使您不为 fib(2) 及更低版本生成,仍然有 16000 个线程在运行。
  • @paxdiablo:一侧有 15 级递归,而另一侧只有 14...所以f(n) = f(n-1) + f(n-2),这与斐波那契本身的递归相同。 (好的,所以你需要加 1,这使得它不完全是斐波那契。但它仍然比 2^n 小很多。)
  • 抱歉,尼莫,+1。我去做一些调查,发现你是对的——n=16 的线程数只有 1,972,而不是我假设的 64K。在我看来还是太多了,但你的计算比我的好。
  • 感谢 Nemo 的详细解答。我做了一个小测试,pthread_created ~10,000 个线程,里面只有一个 while(1) 循环,所以它们不会终止......它确实做到了!确实,操作系统很聪明,只能创建大约 1000 个并运行更小的数字,但它并没有耗尽堆栈。为什么当我生成的数量超过THREAD_MAX 时不会出现段错误,但当我递归执行时会出现?那是最初的问题......
  • @Dervin:我已经修改了我的答案以附加一个可能的解释。如果你做我建议的测试,请告诉我答案。您能否提及您的系统配置(内核数量,32 位或 64 位)?
【解决方案3】:

我要做的第一件事是运行类似printf("%i", PTHREAD_THREADS_MAX); 的语句并查看值是多少;我不认为操作系统的最大线程必然与 pthread 的最大数量相同,尽管我确实看到你说你可以成功实现 16000 个线程而无需递归,所以我只是提到它作为我会检查的东西。

如果PTHREAD_THREADS_MAX 显着> 您正在实现的线程数,我将开始检查 pthread_create() 调用的返回值,看看您是否得到EAGAIN。我怀疑您的问题的答案是您尝试在连接中使用未初始化的线程时遇到了段错误...

另外,正如 paxdiablo 所提到的,您在这里谈论的是2^16 线程的顺序n=16(假设其中一些线程在创建最后一个之前完成);我可能会尝试保留日志以查看每个日志的创建顺序。可能最简单的事情就是使用 (n-1) (n-2) 值作为您的日志项,否则您将不得不使用信号量或类似的东西来保护计数器......

printf 可能会陷入困境,事实上,如果在新线程启动之前允许更多线程完成而实际上影响了事情,我不会感到惊讶,但所以我可能只是使用文件write() 登录;可以只是一个简单的文件,您应该能够通过查看那里的数字模式来了解正在发生的事情。 (等等,假设文件操作是线程安全的;我认为它们是;已经有一段时间了。)

另外,一旦检查了EAGAIN,你可以尝试睡一会儿然后重试;也许它会随着时间的推移而增加,系统只是被大量的线程请求所淹没,并且由于资源不足以外的其他原因而失败;这将验证只是等待和重新启动是否可以让您到达您想要的位置。

终于;我可能会尝试将函数重写为fork()(我知道 fork() 是邪恶的或其他的;)),看看你是否有更好的运气。

:)

【讨论】:

    猜你喜欢
    • 2023-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-26
    • 2017-06-05
    相关资源
    最近更新 更多