【问题标题】:Recursive Fib with Threads, Segmentation Fault?带有线程的递归 Fib,分段错误?
【发布时间】:2011-07-02 10:40:12
【问题描述】:

任何想法为什么它适用于像 0、1、2、3、4... 这样的值以及像 >15 这样的值的段错误? #包括 #包括 #包括

void *fib(void *fibToFind);

main(){
pthread_t mainthread;

long fibToFind = 15;
long finalFib;

pthread_create(&mainthread,NULL,fib,(void*) fibToFind);

pthread_join(mainthread,(void*)&finalFib);

printf("The number is: %d\n",finalFib);
}


void *fib(void *fibToFind){
long retval;

long newFibToFind = ((long)fibToFind);

long returnMinusOne;
long returnMinustwo;

pthread_t minusone;
pthread_t minustwo;

if(newFibToFind == 0 || newFibToFind == 1)
return newFibToFind;

else{
long newFibToFind1 = ((long)fibToFind) - 1;
long newFibToFind2 = ((long)fibToFind) - 2;

pthread_create(&minusone,NULL,fib,(void*) newFibToFind1);
pthread_create(&minustwo,NULL,fib,(void*) newFibToFind2);

pthread_join(minusone,(void*)&returnMinusOne);
pthread_join(minustwo,(void*)&returnMinustwo);

return returnMinusOne + returnMinustwo;

}

}

【问题讨论】:

  • @zneak: ...不是说作业吗?
  • @Mehrdad 我不太相信这个标签,它通常是由在没有真实证据的情况下编辑问题的人添加的(不过,看看编辑日志,这一次似乎是真实的)。
  • 代码似乎漏掉了使用线程的意义……
  • @user186909: 你能在 N=1, 2, ... 上发布运行时间,直到它在单个 CPU 上和任何固定(最好是大你可以做到)CPU数量?
  • 对于那些像我一样对 fib 函数体感到困惑的人,在我输入这个的那一刻,OP 编辑​​了代码并删除了两个递归调用 fib 的 pthread 分支;这段代码使 fib 为小争论产生了正确的答案。检查“已编辑”链接以查看提示问题的原始代码。

标签: c programming-languages pthreads fibonacci


【解决方案1】:

内存不足(堆栈空间不足)或有效的线程句柄?

您需要大量线程,这需要大量堆栈/上下文。 Windows(和 Linux)有一个愚蠢的“大 [连续] 堆栈”想法。

来自 pthreads_create 的文档: “在 Linux/x86-32 上,新线程的默认堆栈大小为 2 兆字节。” 如果您制造 10,000 个线程,则需要 20 Gb 的 RAM。 我构建了一个版本的 OP 程序,它用大约 3500 个 (p) 线程轰炸 在 Windows XP64 上。

请参阅此 SO 线程以获取有关为什么大堆栈是一个非常糟糕的主意的更多详细信息: Why are stack overflows still a problem?

如果您放弃大堆栈,并使用堆分配实现并行语言 用于激活记录 (我们的PARLANSE 是 其中之一)问题消失了。

这是我们在 PARLANSE 中编写的第一个(顺序)程序:

(define fibonacci_argument 45)

(define fibonacci
   (lambda(function natural natural )function 
   `Given n, computes nth fibonacci number'
      (ifthenelse (<= ? 1)
           ?
         (+ (fibonacci (-- ?))
              (fibonacci (- ? 2))
           )+
   )ifthenelse  
   )lambda
 )define

这是在 i7 上运行的执行:

 C:\DMS\Domains\PARLANSE\Tools\PerformanceTest>run fibonaccisequential
 Starting Sequential Fibonacci(45)...Runtime: 33.752067 seconds
 Result: 1134903170

这是第二个,它是平行的:

(define coarse_grain_threshold 30) ; technology constant: tune to amortize fork overhead across lots of work

(define parallel_fibonacci
   (lambda (function natural natural )function 
   `Given n, computes nth fibonacci number'
      (ifthenelse (<= ? coarse_grain_threshold)
           (fibonacci ?)
           (let (;; [n natural ] [m natural ]  )
                   (value (|| (= m (parallel_fibonacci (-- ?)) )=
                              (= n (parallel_fibonacci (- ? 2)) )=
                          )||
                          (+ m n)
                   )value
           )let
       )ifthenelse  
   )lambda
)define

明确的并行性也使程序更容易编写。

我们通过调用 (parallel_fibonacci 45) 测试的并行版本。这里 是在同一个 i7 上运行的执行(可以说有 8 个处理器, 但它实际上是 4 个超线程处理器,所以它真的不是 8 个 等效 CPU):

C:\DMS\Domains\PARLANSE\Tools\PerformanceTest>run fibonacciparallelcoarse
Parallel Coarse-grain Fibonacci(45) with cutoff 30...Runtime: 5.511126 seconds
Result: 1134903170

接近 6+ 的加速,对于非 8 处理器来说还不错。其中之一 这个问题的答案运行了 pthreads 版本;花了“几秒钟” (炸毁)计算 Fib(18),Fib(45) 是 5.5 秒。 这告诉你 pthreads 从根本上来说,做很多细粒度并行的方法很糟糕,因为 它有非常非常高的分叉开销。 (PARLANSE 旨在 尽量减少分叉开销)。

如果您将技术常数设置为零,会发生以下情况(每个调用 到fib):

C:\DMS\Domains\PARLANSE\Tools\PerformanceTest>run fibonacciparallel
Starting Parallel Fibonacci(45)...Runtime: 15.578779 seconds
Result: 1134903170

您可以看到分摊分叉开销是一个好主意,即使您有快速分叉。

Fib(45) 产生 lot 的颗粒。堆分配 激活记录数解决了 OP 的一阶问题(每个有数千个 pthread 1Mb 的堆栈会消耗 GB 的 RAM)。

但是还有第二个问题:2^45 PARLANSE“grains”也会烧掉你所有的记忆 即使您的谷物控制块很小,也只需跟踪谷物。 因此,一旦您拥有“很多”,拥有一个可以限制分叉的调度程序会很有帮助 (对于“很多”的一些定义,明显少于 2^45)谷物以防止 使用“颗粒”跟踪数据结构淹没机器导致并行性爆炸。 当谷物数量低于阈值时,它必须解除对分叉的限制 同样,为了确保物理上总是有很多逻辑的、并行的工作 CPU 要做。

【讨论】:

  • +1 指出了我发现的 3 个潜在问题,但我想指出线程可能不是问题。
【解决方案2】:

您没有检查错误 - 特别是来自 pthread_create() 的错误。当pthread_create() 失败时,pthread_t 变量未定义,后续的pthread_join() 可能会崩溃。

如果您确实检查了错误,您会发现pthread_create() 失败了。这是因为您尝试生成近 2000 个线程 - 使用默认设置,这将需要单独分配 16GB 的线程堆栈。

你应该修改你的算法,使它不会产生那么多线程。

【讨论】:

    【解决方案3】:

    我尝试运行您的代码,遇到了几个惊喜:

    printf("The number is: %d\n", finalFib);
    

    此行有一个小错误:%d 表示 printf 需要 int,但传递了 long int。在大多数平台上这是相同的,或者无论如何都会有相同的行为,但是从迂腐的角度来说(或者如果你只是想阻止警告出现,这也是一个非常崇高的理想),你应该使用 %ld 代替,这将需要一个long int

    另一方面,您的 fib 函数似乎不起作用。在我的机器上测试它,它没有崩溃,但它产生1047,这不是斐波那契数。仔细观察,您的程序似乎在几个方面不正确:

    void *fib(void *fibToFind)
    {
        long retval; // retval is never used
    
        long newFibToFind = ((long)fibToFind);
    
        long returnMinusOne; // variable is read but never initialized
        long returnMinustwo; // variable is read but never initialized
    
        pthread_t minusone; // variable is never used (?)
        pthread_t minustwo; // variable is never used
    
        if(newFibToFind == 0 || newFibToFind == 1)
            // you miss a cast here (but you really shouldn't do it this way)
            return newFibToFind;        
        else{
            long newFibToFind1 = ((long)fibToFind) - 1; // variable is never used
            long newFibToFind2 = ((long)fibToFind) - 2; // variable is never used
            // reading undefined variables (and missing a cast)
            return returnMinusOne + returnMinustwo;
    
        }
    }
    

    始终注意编译器警告:当您收到一个警告时,通常,您真的在做一些可疑的事情。

    也许你应该稍微修改一下算法:现在,你的函数所做的只是返回两个未定义值的总和,因此我之前得到的 1047。

    使用递归算法实现斐波那契套件意味着您需要再次调用该函数。正如其他人所指出的,这是一种非常低效的方法,但它很容易,所以我想所有的计算机科学教师都以它为例。

    正则递归算法如下所示:

    int fibonacci(int iteration)
    {
        if (iteration == 0 || iteration == 1)
            return 1;
    
        return fibonacci(iteration - 1) + fibonacci(iteration - 2);
    }
    

    我不知道您应该在多大程度上使用线程——只是在辅助线程上运行算法,还是为每次调用创建新线程?现在让我们假设第一个,因为它更简单。

    将整数转换为指针,反之亦然,这是一种不好的做法,因为如果您尝试从更高的层次来看待事物,它们应该会有很大的不同。整数做数学运算,指针解析内存地址。它恰好起作用,因为它们以相同的方式表示,但实际上,你不应该这样做。相反,您可能会注意到为运行新线程而调用的函数接受 void* 参数:我们可以使用它来传达输入的 wherewhere输出将是。

    因此,在我之前的 fibonacci 函数的基础上,您可以将此代码用作线程主例程:

    void* fibonacci_offshored(void* pointer)
    {
        int* pointer_to_number = pointer;
        int input = *pointer_to_number;
        *pointer_to_number = fibonacci(input);
        return NULL;
    }
    

    它需要一个指向整数的指针,并从中获取其输入,然后将其写入输出。1 然后您将像这样创建线程:

    int main()
    {
        int value = 15;
        pthread_t thread;
    
        // on input, value should contain the number of iterations;
        // after the end of the function, it will contain the result of
        //  the fibonacci function
        int result = pthread_create(&thread, NULL, fibonacci_offshored, &value);
    
        // error checking is important! try to crash gracefully at the very least
        if (result != 0)
        {
            perror("pthread_create");
            return 1;
        }
    
        if (pthread_join(thread, NULL)
        {
            perror("pthread_join");
            return 1;
        }
    
        // now, value contains the output of the fibonacci function
        // (note that value is an int, so just %d is fine)
        printf("The value is %d\n", value);
        return 0;
    }
    

    如果您需要从新的不同线程调用 Fibonacci 函数(请注意:这不是我的建议,其他人似乎也同意我的观点;它只会在足够多的迭代中崩溃),您'首先需要将fibonacci 函数与fibonacci_offshored 函数合并。它会大大增加它的体积,因为处理线程比处理常规函数更重。

    void* threaded_fibonacci(void* pointer)
    {
        int* pointer_to_number = pointer;
        int input = *pointer_to_number;
    
        if (input == 0 || input == 1)
        {
            *pointer_to_number = 1;
            return NULL;
        }
    
        // we need one argument per thread
        int minus_one_number = input - 1;
        int minus_two_number = input - 2;
    
        pthread_t minus_one;
        pthread_t minus_two;
        // don't forget to check! especially that in a recursive function where the
        // recursion set actually grows instead of shrinking, you're bound to fail
        // at some point
        if (pthread_create(&minus_one, NULL, threaded_fibonacci, &minus_one_number) != 0)
        {
            perror("pthread_create");
            *pointer_to_number = 0;
            return NULL;
        }
        if (pthread_create(&minus_two, NULL, threaded_fibonacci, &minus_two_number) != 0)
        {
            perror("pthread_create");
            *pointer_to_number = 0;
            return NULL;
        }
    
        if (pthread_join(minus_one, NULL) != 0)
        {
            perror("pthread_join");
            *pointer_to_number = 0;
            return NULL;
        }
    
        if (pthread_join(minus_two, NULL) != 0)
        {
            perror("pthread_join");
            *pointer_to_number = 0;
            return NULL;
        }
    
        *pointer_to_number = minus_one_number + minus_two_number;
        return NULL;
    }
    

    现在您有了这个庞大的函数,调整您的 main 函数将非常容易:只需将对 fibonacci_offshored 的引用更改为 threaded_fibonacci

    int main()
    {
        int value = 15;
        pthread_t thread;
    
        int result = pthread_create(&thread, NULL, threaded_fibonacci, &value);
    
        if (result != 0)
        {
            perror("pthread_create");
            return 1;
        }
        pthread_join(thread, NULL);
    
        printf("The value is %d\n", value);
        return 0;
    }
    

    您可能被告知线程可以加速并行进程,但是在某些地方设置线程比运行其内容更昂贵是有限制的。 这是这种情况的一个很好的例子:程序的线程版本比非线程版本的运行速度要慢得多。

    出于教育目的,当所需的迭代次数为 18 时,此程序会在我的机器上耗尽线程,并且需要几秒钟才能运行。相比之下,使用迭代实现,我们永远不会用完线程,而且我们会在几毫秒内得到答案。它也相当简单。这将是一个很好的例子,说明如何使用更好的算法解决许多问题。

    此外,出于好奇,看看它是否在您的机器上崩溃以及在哪里/如何崩溃会很有趣。

    1。通常,您应该尽量避免在输入时的值和函数返回后的值之间改变变量的含义。例如,在这里,输入时,变量是我们想要的迭代次数;在输出上,它是函数的结果。这是两个非常不同的含义,这不是一个很好的做法。我不想使用动态分配通过void* 返回值返回值。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-11-30
      • 1970-01-01
      • 1970-01-01
      • 2017-06-05
      • 2018-03-30
      • 1970-01-01
      相关资源
      最近更新 更多