Ошибка сегментации для pthreads в рекурсивном вызове - PullRequest
1 голос
/ 12 июля 2011

Учитывая приведенный ниже код, я получаю ошибку сегментации, если я запускаю ее с n> 16.

Я думаю, что это как-то связано со стеком, но я не могу понять это.Кто-нибудь может мне помочь?Код не мой, и действительно не важно.Я просто хотел бы, чтобы кто-то помог мне с тем, что происходит. Этот вопрос SO очень похож, но недостаточно информации (человек, который публикует ответ, кратко говорит о проблеме, но затем продолжает говорить о другом языке).Кроме того, обратите внимание, что с двумя гигабайтами и без рекурсии я могу (если я делаю это правильно) успешно создать более 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;
}

Ответы [ 3 ]

4 голосов
/ 12 июля 2011

Это не хороший способ сделать последовательность Фибоначчи: -)

Ваш первый поток запускает два других, каждый из которых запускает два других и так далее.Поэтому, когда n > 16, вы можете получить очень большое количество потоков (в тысячах) (a) .

Если ваш процессор не имеет way moreчем мои ядра, вы будете тратить свое время на запуск тысяч потоков для такой задачи, связанной с процессором.Для задач, связанных только с процессором, лучше иметь столько потоков, сколько имеется физических механизмов исполнения (ядер или процессоров).Очевидно, что это меняется, когда вы не ограничены только процессором.


Если вам нужен эффективный рекурсивный (не поточный) калькулятор Фибоначчи, вы должны использовать что-то вроде (псевдокод):

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) Первоначально я думал, что это будет 2 16 , но, как показывает следующая программа (и благодаря 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

Теперь обратите внимание, что хотя я сказал, что это не как плохо, я не говорил, что это хорошо :-)Даже две тысячи потоков - это достаточная нагрузка на систему, каждый из которых имеет свои собственные структуры ядра и стеки.И вы можете видеть, что, хотя увеличение начинается с малого, оно быстро ускоряется до такой степени, что становится неуправляемым.И это не то, что число 32 и велико - это всего лишь смайлик, превышающий два миллиона.

Таким образом, итоговая черта остается неизменной: используйте рекурсию там, где это имеет смысл (где вы можете уменьшитьсравнительно быстро ищите пространство, чтобы не исчерпать пространство стека), и используйте threds там, где это имеет смысл (когда вы не заканчиваете тем, что запускаете так много, что перегружаете ресурсы операционной системы).

2 голосов
/ 12 июля 2011

Черт возьми, это может сделать ответ.

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

Во-вторых, я мог бы поклясться, что Linux glibc по умолчанию выделяет примерно 2 мегабайта стека на поток (настраивается с помощью pthread_attr_setstacksize ). Конечно, это только виртуальная память, но в 32-разрядной системе, которая все еще ограничивает вас до ~ 2000 потоков.

Наконец, я считаю, что правильная оценка количества потоков, которые будут порождены, в основном fib(n) сама по себе (как хорошо рекурсивно). Или примерно phi^n, где phi равно (1+sqrt(5))/2. Таким образом, число потоков здесь ближе к 2000, чем к 65000, что согласуется с моей оценкой того, где в 32-разрядной системе не хватит виртуальной машины.

[править]

Чтобы определить размер стека по умолчанию для новых потоков в вашей системе, запустите эту программу:

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]

Ах, я вижу, суть вашего вопроса заключается в следующем (поднято из комментариев):

Спасибо Немо за подробный ответ. Я сделал небольшой тест и pthread_created ~ 10000 потоков с циклом just (1) внутри, так они не заканчиваются ... и это произошло! Правда что ОС была умная только создать около 1000 и запустить еще меньшее число, но это не выбежал из стека. Почему я не получаю segfault при генерации намного больше, чем THREAD_MAX, но я делаю, когда делаю это рекурсивно?

Вот мое предположение.

У вас есть только несколько ядер. В любое время ядро ​​должно решить , какие потоки будут запущены. Если у вас есть, скажем, 2 ядра и 500 потоков, то любой конкретный поток будет работать только 1/250 времени. Таким образом, ваш основной цикл, порождающий новые потоки, не будет запускаться очень часто. Я даже не уверен, является ли планировщик ядра «справедливым» по отношению к потокам в рамках одного процесса, поэтому вполне возможно, что при 1000 потоках основной поток вообще никогда не сможет работать.

По крайней мере, каждый поток, выполняющий while (1);, будет работать на 1/HZ в своем ядре, прежде чем отказаться от своего временного интервала. Вероятно, это 1 мс, но она может достигать 10 мс в зависимости от того, как настроено ваше ядро. Поэтому, даже если планировщик исправен, ваш основной поток будет запускаться только раз в секунду, когда у вас есть тысячи потоков.

Поскольку новые потоки создает только основной поток, скорость создания потоков замедляется и может даже останавливаться.

Попробуй это. Вместо while (1); для дочерних потоков в вашем эксперименте, попробуйте while (1) pause();. (pause от unistd.h.) Это будет держать дочерние потоки заблокированными и должно позволить основному потоку продолжать шлифовать, создавая новые потоки, приводя к вашей аварии.

И снова, пожалуйста, проверьте, что pthread_create возвращает.

0 голосов
/ 12 июля 2011

Первое, что я хотел бы сделать, это запустить оператор типа printf("%i", PTHREAD_THREADS_MAX); и посмотреть, каково значение; я не думаю, что максимальное количество потоков в операционной системе обязательно такое же, как максимальное число потоков pthreads, хотя я вижу, что вы говорите, что можете успешно достичь 16000 потоков без рекурсии, поэтому я просто упоминая это как что-то, что я бы вообще проверил.

должно PTHREAD_THREADS_MAX быть значительно> числом потоков, которых вы достигаете, я бы начал проверять возвращаемые значения вызовов pthread_create (), чтобы увидеть, получаете ли вы EAGAIN. Я подозреваю, что ответ на ваш вопрос заключается в том, что вы получаете segfault от попытки использовать неинициализированный поток в соединении ...

также, как упомянул paxdiablo, вы говорите о порядке 2^16 потоков в n=16 здесь (чуть меньше, если предположить, что некоторые из них заканчивают работу до того, как будут созданы последние); Я, вероятно, попытался бы сохранить журнал, чтобы увидеть, в каком порядке каждый из них был создан. вероятно, проще всего было бы просто использовать значения (n-1) (n-2) в качестве элементов журнала, в противном случае вам придется использовать семафор или аналогичный объект для защиты счетчика ...

printf может застопориться, на самом деле, я не удивлюсь, если это действительно повлияет на ситуацию, позволив большему количеству потоков завершить работу до того, как начнутся новые, но поэтому я, вероятно, просто войду в систему, используя файл write(); это может быть простой файл, вы сможете почувствовать происходящее, взглянув на шаблоны чисел. (подождите, это предполагает, что операции с файлами являются потокобезопасными; я думаю, что они были; это было давно.)

также, однажды проверив EAGAIN, вы можете попытаться немного поспать и повторить попытку; возможно, со временем она будет увеличиваться, и система просто перегружена огромным количеством запросов потоков и терпит неудачу по какой-то другой причине, кроме нехватки ресурсов; это проверит, может ли просто ожидание и перезапуск привести вас туда, куда вы хотите.

наконец; я мог бы попытаться переписать функцию как fork() (я знаю, что fork () - зло или что-то в этом роде;)) и посмотрю, повезет ли вам больше там.

:)

...