LINUX.ORG.RU

Учебный фрагмент кода по многопоточному программированию

 


0

4

Добрый день! Изучаю «Современные операционные системы» Таненбаума и дошёл до многопоточного программирования. Там приведён фрагмент кода:

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

#define NUMBER_OF_THREADS 10

void *print_hello_world(void *tid)
{
    printf("Привет, мир. Тебя приветствует поток № %d\n", (int) tid);
    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    pthread_t threads[NUMBER_OF_THREADS];
    int status, i;

    for (i = 0; i < NUMBER_OF_THREADS; ++i) {
        printf("Это основная программа. Создание потока № %d\n", i);
        status = pthread_create(&threads[i], NULL, print_hello_world, (void *) i);

        if (status != 0) {
            printf("Жаль, функция pthread_create вернула код ошибки %d\n", status);
            exit(-1);
        }
    }
    exit(0);
}
При запуске иногда случается так, что в выводе присутствуют две строки:
Привет, мир. Тебя приветствует поток № 9
Привет, мир. Тебя приветствует поток № 9
При этом, для потоков от 0 до 8 присутствует ровно по одной строке. Я также заметил, что при таком фрагменте кода вывод для 9 потока иногда отсутствует, и сделал выводы, что это из-за exit в main - процесс со всеми потоками может завершиться, прежде чем 9 поток успеет выполниться. Я поменял exit на pthread_exit(NULL), и после этого дублирующиеся строки больше не возникают. Подскажите, пожалуйста, чем могло быть вызвано дублирование вывода. Заранее спасибо.

★★

Последнее исправление: beastie (всего исправлений: 1)

При запуске иногда случается так, что в выводе присутствуют две строки:

Вот это совершенно непонятно. Какая ось и версия, версия ядра, компилятор (gcc или что-то ещё), версия компилятора, версия libc и libpthread, опции (командная строка) компиляции? Я пробовал воспроизвести это на 64-битном gcc с опциями оптимизации от 0 до 3:

gcc -o test_pthreads -l pthread test_pthreads.c -O3

- ничего не вышло.

Я также заметил, что при таком фрагменте кода вывод для 9 потока иногда отсутствует

А у меня иногда твои потоки вообще не успевают ничего вывести (особенно когда код оптимизирован):

Это основная программа. Создание потока № 0
Это основная программа. Создание потока № 1
Это основная программа. Создание потока № 2
Это основная программа. Создание потока № 3
Это основная программа. Создание потока № 4
Это основная программа. Создание потока № 5
Это основная программа. Создание потока № 6
Это основная программа. Создание потока № 7
Это основная программа. Создание потока № 8
Это основная программа. Создание потока № 9

Как правильно сказал тебе i-rinat, добавь ожидание в цикле pthread_join() для каждого запущенного потока. Можно просто добавить sleep(1) вместо pthread_join(), но sleep - это костыль. pthread_join будет честно дожидаться завершения запущенных потоков, а sleep просто подождёт заданное чило секунд и завершится. В одних случаях этого может оказаться слишком много (тогда программа отработает корректно, но дольше, чем необходимо), а в других - мало (тогда часть потоков могут не успеть выполниться). Ну и ещё sleep может быть досрочно прервана другим сигналом.

Касательно приведения указателя к целому. Да, на 64 битной системе с 32-битным int выскочит предупреждение. Чтоб от него избавиться, вместо int нужно подставить long. В общем случае необходимо, чтоб целое, к которому ты приводишь, и указатель были одного размера. Однако т. к. ты всё равно используешь очень небольшие числа, которые поместятся в любой тип, хоть в char, никаких неприятных последствий и побочных эффектов, кроме предупреждений компилятора, у тебя не будет. Можно было бы, конечно, malloc'ом выделить память для каждого потока размером в sizeof(int), записать в эту память нужный номер и передать его через void* i, а потом, по завершении потока, освободить эту память. Но зачем так много действий, если нужно передать всего одну целочисленную переменную? Это Си, и для Си это нормальная практика. Просто всегда надо понимать, что ты делаешь, помнить о возможных побочных эффектах на других архитектурах, и разумно сочетать критерии переносимости и эффективности. А если тебе понадобится 100% переносимость, используй яву, но про эффективность тогда можешь забыть.

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

Касательно приведения указателя к целому. Да, на 64 битной системе с 32-битным int выскочит предупреждение. Чтоб от него избавиться, вместо int нужно подставить long. В общем случае необходимо, чтоб целое, к которому ты приводишь, и указатель были одного размера.

uint*_t для вот этой всей катавасии не проще использовать? Если нет задачи выдерживать С89.

SkyMaverick ★★★★★
()
Ответ на: комментарий от aureliano15

А у меня иногда твои потоки вообще не успевают ничего вывести (особенно когда код оптимизирован)

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

Iron_Bug ★★★★★
()
Ответ на: комментарий от aureliano15

-l pthread

Не делай так. Нужно использовать -pthread, а не -lpthread. Первая директива может включать в себя ещё и различные директивы препроцессора, которые включают потокобезопасные пути в заголовочных файлах libc. Гарантируется, что -pthread будет нормально работать всегда, а для -lpthread таких гарантий нет.

ничего не вышло

Там состояние гонки. Такой баг только случайно воспроизводится. Попробуй запускать много раз подряд. У меня воспроизводилось примерно один раз из десяти. Можно попробовать запустить под Valgrind, чтобы внести разброд и шатания.

i-rinat ★★★★★
()
Ответ на: комментарий от Iron_Bug

не успевают ничего вывести (особенно когда код оптимизирован)

это не столько оптимизация кода, сколько загрузка системы.

Да, похоже так и есть. Попробовал на другом компе, - всё (или почти всё, когда как) успевается независимо от уровня оптимизации.

без стандартной библиотеки это вообще практически сплошная кроссплатформа в чистом виде.

Не всегда. Например, тот же int может быть 16, 32 или 64 бита. А указатели могут быть 16, 16:16, 32 и 64. При этом указатели 16:16 могут быть не равны друг другу, но указывать на один объект (far), а могут быть нормализованы (huge). Я уже не говорю о порядке байт в слове. Ну и плавающая точка до принятия стандартов вообще была у всех какая попало, но и сейчас у Intel за счёт long double (забыл, как оно называется в терминологии Intel), к которому всё приводится при вычислениях, будет отличаться от вычислений на других процессорах (последняя проблема по умолчанию относится и к яве, хотя там её можно пофиксить специальной инструкцией). И всё это только числа, без каких-то библиотек.

aureliano15 ★★
()
Ответ на: комментарий от i-rinat

-l pthread

Не делай так. Нужно использовать -pthread, а не -lpthread.

Спасибо за информацию.

ничего не вышло

Попробуй запускать много раз подряд. У меня воспроизводилось примерно один раз из десяти.

Да, у меня тоже воспроизвелось.

Это основная программа. Создание потока № 0
Это основная программа. Создание потока № 1
Это основная программа. Создание потока № 2
Это основная программа. Создание потока № 3
Привет, мир. Тебя приветствует поток № 1
Привет, мир. Тебя приветствует поток № 3
Привет, мир. Тебя приветствует поток № 0
Это основная программа. Создание потока № 4
Привет, мир. Тебя приветствует поток № 2
Это основная программа. Создание потока № 5
Привет, мир. Тебя приветствует поток № 4
Это основная программа. Создание потока № 6
Привет, мир. Тебя приветствует поток № 5
Это основная программа. Создание потока № 7
Это основная программа. Создание потока № 8
Привет, мир. Тебя приветствует поток № 6
Привет, мир. Тебя приветствует поток № 7
Это основная программа. Создание потока № 9
Привет, мир. Тебя приветствует поток № 8
Привет, мир. Тебя приветствует поток № 9
Привет, мир. Тебя приветствует поток № 9

Самое смешное, что сначала запустил 1000 раз подряд таким способом:

(i=0; while [ $i -lt 1000 ]; do echo -e "\nPass $i:\n_____________________\n\n"; ./test_pthread; let i+=1; done) | awk 'BEGIN {old=""} {if(old==$0 && $0!="") print old " <==> " $0; old=$0;}'

и ничего не воспроизвелось. Хотел уже было написать, что всё равно не воспроизводится, но напоследок запустил 1001-й раз просто, без циклов, и получил. :-) А потом снова получил уже с 7-м потоком:

Это основная программа. Создание потока № 7
Это основная программа. Создание потока № 8
Привет, мир. Тебя приветствует поток № 6
Это основная программа. Создание потока № 9
Привет, мир. Тебя приветствует поток № 7
Привет, мир. Тебя приветствует поток № 7

В общем, баг воспроизводится довольно часто, но автоматизации в цикле почему-то не поддаётся. :-)

это загадка

В исходниках glibc комментарий есть на эту тему. Загадок нет.

А можно цитату? А то досконально изучить все исходники в поисках этого комментария меня просто не хватит, а хотелось бы понять, в чём тут дело.

aureliano15 ★★
()
Ответ на: комментарий от SkyMaverick

uint*_t

Да, это ещё лучше, хотя всех проблем не решает: размер целого теперь одинаков на всех платформах, но размеры указателей всё равно различаются. Чтобы подавить предупреждения раз и на всегда, можно использовать объединения void* и int. Вот такой вариант gcc компиляет без предупреждений:

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

#define NUMBER_OF_THREADS 10

union IP
{
 int i;
 void *p;
};

void *print_hello_world(void *tid)
{
    union IP ip;

    ip.p=tid;
    printf("Привет, мир. Тебя приветствует поток № %d\n", ip.i);
    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
    pthread_t threads[NUMBER_OF_THREADS];
    int status;
    union IP ip;

    for (ip.i = 0; ip.i < NUMBER_OF_THREADS; ++ip.i) {
        printf("Это основная программа. Создание потока № %d\n", ip.i);
        status = pthread_create(&threads[ip.i], NULL, print_hello_world, ip.p);

        if (status != 0) {
            printf("Жаль, функция pthread_create вернула код ошибки %d\n", status);
            exit(-1);
        }
    }
    exit(0);
}

Правда, теперь в функции print_hello_world лишнее присваивание, но это копейки.

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

А потом снова получил уже с 7-м потоком

Дублируется последняя линия.

А можно цитату?

Ну так не интересно же.

досконально изучить все исходники в поисках этого комментария меня просто не хватит

Там минут за 15 можно уложиться, не спеша. Нужно поймать баг в отладчике и посмотреть, что откуда куда двигается.

i-rinat ★★★★★
()
Ответ на: комментарий от aureliano15
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NUMBER_OF_THREADS 10

//Ненужное ненужно.
//union IP
//{
// int i;
// void *p;
//};

void *print_hello_world(void *tid)
{
    printf("Привет, мир. Тебя приветствует поток № %d\n", (int)tid);
    //pthread_exit(NULL); При штатном завершении это не нужно
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t threads[NUMBER_OF_THREADS];
    int i;
    int retval = 0;

    for (i = 0; i < NUMBER_OF_THREADS; ++i) {
        int status;
        printf("Это основная программа. Создание потока № %d\n", i);
        status = pthread_create(&threads[i], NULL, print_hello_world, (void*)i);

        if (status != 0) {
            printf("Жаль, функция pthread_create вернула код ошибки %d\n", status);
            //exit(-1); Это нештатное завершение при работающих потоках.
            retval = -1;
            break;
        }
    }
    //Ждём завершения всех созданных потоков
    for(--i; i >= 0; --i)
        ptread_join(threads[i], NULL);

    //exit(0); При штатном завершении это не нужно
    return retval;
}
alexku
()
Ответ на: комментарий от alexku

//Ненужное ненужно.

[skip]

//pthread_exit(NULL); При штатном завершении это не нужно
return NULL;

[skip]

ptread_join(threads, NULL);

Так мы же не программу за тс'а переписывали, а говорили о том, как подавить предупреждения компилятора о неравенстве размеров void* и int.

А про pthread_join ему с самого начала сказали. И про return'ы вместо *exit'ов кто-то уже говорил, хотя на это-то как раз gcc не ругался. А по сути что return'ы, что *exit'ы - один хрен.

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

говорили о том, как подавить предупреждения компилятора о неравенстве размеров void* и int

Ну, тогда union - то что надо. Но можно, как вариант, int заменить на long, который 4 байта в 32-бит и 8 байт в 64-бит gcc.

alexku
()
Ответ на: комментарий от alexku

можно, как вариант, int заменить на long

И об этом говорили. :-)

на 64 битной системе с 32-битным int выскочит предупреждение. Чтоб от него избавиться, вместо int нужно подставить long. В общем случае необходимо, чтоб целое, к которому ты приводишь, и указатель были одного размера.

uint*_t для вот этой всей катавасии не проще использовать? Если нет задачи выдерживать С89.

Да, это ещё лучше, хотя всех проблем не решает: размер целого теперь одинаков на всех платформах, но размеры указателей всё равно различаются. Чтобы подавить предупреждения раз и на всегда, можно использовать объединения void* и int.

aureliano15 ★★
()
Ответ на: комментарий от anonymous

size_t, ptrdiff_t, uintptr_t

Это самый переносимый и очевидный вариант, который никому не пришёл в голову! :-)

aureliano15 ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.