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)

Ответ на: комментарий от anonymous

Если тебе нужно гарантированно одинаковое поведение на любой архитектуре, пиши на яве. А си создавался немного для других целей.

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

проблема с кодом действительно не в этом. я программирую на С уже более 20 лет, профессионально. в приведённом коде нет никакого «нарушения спецификации». есть просто выход из программы до завершения дочерних потоков. но это не запрещено. это может быть и аварийный выход. и в поток передаётся то, что надо. вне зависимости от оптимизаций. потому что передаётся обычный int.
но если одолевает паранойя и ты не доверяешь стандартам С - смотри ассемблерный вывод. времени у тебя дофига, очевидно.

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 2)
Ответ на: комментарий от aureliano15

на С тоже можно писать кроссплатформу. без стандартной библиотеки это вообще практически сплошная кроссплатформа в чистом виде. а с использованием библиотеки просто нужно соблюдать больше формальностей и знать об особенностях реализации некоторых функций в разных системах.

Iron_Bug ★★★★★
()
Последнее исправление: Iron_Bug (всего исправлений: 1)
Ответ на: комментарий от Iron_Bug

Суть в том, что воспроизвести тебе не удалось. Т.е. «у меня всё работает». И ты начинаешь выдумывать несуществующие проблемы, хотя очевидно, что где-то что-то не так соптимизировалось (или ТС лжет).

Спецификация это объявление процедуры. И оно нарушено вот этим: (void *) i.

но если одолевает паранойя и ты не доверяешь стандартам С - смотри ассемблерный вывод

Ассемблер компилируется 1:1. Это его свойство. C/C++ код может преобразовываться по-разному. поэтому при любых разборах полётов нужно смотреть генерируемый код. Как можно этого не знать после 20 лет и что ты подразумеваешь под «профессионально» вопрос.

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

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

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

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

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

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

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

-l pthread

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

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

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

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

нет. я лишь проверила, есть ли дублирование флуша буфера стандартного вывода на musl. с некоторой вероятностью, его нет. по крайней мере, при разной нагрузке на проц у меня не возникло ни одного дублирования. но можно просто залезть в код и там рыться. смысл не в этом. в стандарте не описано детальное поведение функции exit. как не описана реализация работы со стандартным вводом-выводом. поэтому в данном случае это UB, хотя никакие «спецификации» не нарушены. просто частный случай аварийного выхода из программы. было любопытно посмотреть, есть ли разница между musl и glibc.
в С есть стандарты. и разные реализации компиляторов и библиотек им соответствуют. не надо впаривать ассемблер там, где он не нужен. я с ним много работала, но только тогда, когда это актуально. а у тебя просто паранойя. и ты до сих пор принципиально не понимаешь, в чём проблема данного кода. не знаю, может рекомпиляция стандартной библиотеки с ассемблерным выводом тебе как-то поможет. по крайней мере, я не буду читать твой флуд очень долго, потому что библиотека жирная.

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

Опять «у меня всё работает».

не надо впаривать ассемблер там, где он не нужен

В данном случае вывод компилятора. Это мог быть и не ассемблер, а какой-нибудь промежуточный код. Суть в том, что на этом этапе можно не гадать - ассемблер преобразуется 1:1 без оптимизаций. Это окончательный и бесповоротный результат. И последний в цепочке, который можно читать.

хотя никакие «спецификации» не нарушены

Не имеешь права так утверждать, если ты не разработчик pthreads. И причём именно этой конкретной процедуры.

детальное поведение функции exit

Воспроизвести не удалось, но мы будем гадать до последнего. Как exit может быть связан с выводимям сообщением?

а у тебя просто паранойя

И это ты не можешь утверждать. Даже если у тебя есть диплом психолога. Ты ведь меня в глаза не видела. Вообще я смотрю ты очень вольно обращаешься с фактами, для программиста. 20 лет разработки, говоришь? Ну ну.

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

А си создавался немного для других целей.

Каких же? Список в студию.

Для написания на нём ОС UNIX. И именно поэтому в си разрешаются трюки, считающиеся моветоном в других языках.

aureliano15 ★★
()
Ответ на: комментарий от 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 ★★★★★
()
Ответ на: комментарий от anonymous

Спецификация это объявление процедуры. И оно нарушено вот этим: (void *) i.

Разупорись уже. Передача целочисленного значения через приведение к void*, размер которого вписывается в размер указателя - обычная практика. Попытка передать в поток указатель на стек - признак некомпетентности.

Вот целый список признаков:

Сойдёмся на адресе. Нужно передавать адрес.

Кому нужно? Какой адрес?

Но тебе надо передавать по ссылке!

Какие ссылки в сишном коде?

И вам здрасьте. Стек это область памяти.

Которая будет похерена «как только так сразу» при выходе из создавшей поток функции.

Да. Просто совпало, что адрес и int по размеру одинаковые.

Не совпало, а так задумано.

alexku
()
Ответ на: комментарий от 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 ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.