Рекурсивный Fib с потоками, ошибка сегментации?

Любые идеи, почему он отлично работает для значений, таких как 0, 1, 2, 3, 4 ... и ошибок seg для таких значений, как> 15? #include #include #include

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;

}

}

person cnandreu    schedule 23.02.2011    source источник
comment
@zneak: ... там не написано домашнее задание?   -  person user541686    schedule 23.02.2011
comment
@Mehrdad Я не слишком доверяю тегу, обычно его добавляют люди, редактирующие вопрос без реальных доказательств (хотя, глядя на журнал редактирования, на этот раз он кажется настоящим).   -  person zneak    schedule 23.02.2011
comment
Кажется, что в коде отсутствует смысл использования потоков ...   -  person    schedule 23.02.2011
comment
@ user186909: Можете ли вы опубликовать время работы этого для N = 1, 2, ... до точки, где он взорвется, на одном процессоре и на любом фиксированном (желательно настолько большом, насколько вы можете) количестве Процессоры?   -  person Ira Baxter    schedule 23.02.2011
comment
Для тех из вас, кто так же сбит с толку, как я, тело функции fib в тот момент, когда я набираю это, OP отредактировал код и удалил две вилки pthread, которые рекурсивно вызывали fib; этот код заставлял fib давать правильный ответ для небольших аргументов. Проверьте отредактированную ссылку, чтобы увидеть исходный код, который вызвал вопрос.   -  person Ira Baxter    schedule 23.02.2011
comment
... и в этот момент он, кажется, положил их обратно.   -  person Ira Baxter    schedule 23.02.2011
comment
Глядя на этот код, я заметил действительно ужасный сбой: значение, переданное в fib, является long, но приводится к указателю на сайте вызова pthread_craete, потому что, по всей видимости, это то, что будет передавать pthread_create; аналогично для возвращаемого значения. Проблема в том, что размер fib as a long может не совпадать с размером указателя. Эта программа взрывается до того, как пройденные значения становятся достаточно большими, чтобы это стало проблемой.   -  person Ira Baxter    schedule 23.02.2011


Ответы (3)


Не хватает памяти (не хватает места для стеков) или допустимых дескрипторов потоков?

Вы запрашиваете очень много потоков, которые требуют большого количества стека / контекста. В Windows (и Linux) есть дурацкая идея «большого [непрерывного] стека».

Из документации на pthreads_create: «В Linux / x86-32 размер стека по умолчанию для нового потока составляет 2 мегабайта». Если вы производите 10 000 потоков, вам потребуется 20 ГБ оперативной памяти. Я создал версию программы OP, и она взорвала около 3500 (p) потоков в Windows XP64.

См. Этот поток SO для получения дополнительной информации о том, почему большие стеки - действительно плохая идея: Почему переполнение стека все еще проблема?

Если вы откажетесь от больших стеков и реализуете параллельный язык с выделением кучи для записей активации (наш 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 эквивалентных процессоров):

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

Ускорение около 6+, неплохо для процессоров с не совсем восьмёркой. Один из других ответов на этот вопрос касался версии pthreads; потребовалось «несколько секунд» (чтобы взорвать) вычисление Фиб (18), а это 5,5 секунды для Фиб (45). Это говорит о том, что pthreads - это принципиально плохой способ реализовать много мелкозернистого параллелизма, потому что он имеет действительно очень высокие накладные расходы на разветвление. (PARLANSE разработан, чтобы минимизировать накладные расходы на разветвление).

Вот что произойдет, если вы установите технологическую константу равной нулю (разветвляется при каждом вызове fib):

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

Вы можете видеть, что амортизация накладных расходов на вилку - хорошая идея, даже если у вас есть быстрые вилы.

Fib (45) производит партию зерен. Распределение записей активации в куче решает проблему первого порядка OP (тысячи потоков pthread каждая с 1 МБ стека сжигают гигабайты ОЗУ).

Но есть проблема второго порядка: 2 ^ 45 PARLANSE "grains" тоже сожгут всю вашу память, просто отслеживая зерна, даже если ваш блок контроля зерна крошечный. Поэтому полезно иметь планировщик, который дросселирует вилки, когда у вас есть "много" (для некоторого определения "много" значительно меньше 2 ^ 45) зерен, чтобы предотвратить взрыв параллелизма от заваливания машины данными отслеживания "зернистости". конструкции. Он также должен отключать вилки, когда количество зерен падает ниже порогового значения, чтобы гарантировать, что физическим ЦП всегда будет много логической, параллельной работы.

person Ira Baxter    schedule 23.02.2011
comment
+1 за указание на 3 потенциальные проблемы, которые я тоже обнаружил, но я хотел бы отметить, что, вероятно, проблема не в потоках. - person zneak; 23.02.2011

Вы не проверяете на наличие ошибок - в частности, от pthread_create(). При сбое pthread_create() переменная pthread_t остается неопределенной, и последующий pthread_join() может дать сбой.

Если вы все-таки проверите ошибки, вы обнаружите, что pthread_create() не работает. Это связано с тем, что вы пытаетесь сгенерировать почти 2000 потоков - при настройках по умолчанию для этого потребуется выделить только 16 ГБ стеков потоков.

Вам следует пересмотреть свой алгоритм, чтобы он не генерировал так много потоков.

person caf    schedule 23.02.2011

Я попытался запустить ваш код и наткнулся на несколько сюрпризов:

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*: мы можем использовать его, чтобы передать как где вход, так и где выход будет быть.

Итак, основываясь на моей предыдущей функции 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_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*.

person zneak    schedule 23.02.2011