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

Пожалуй, к этому примеру я вернусь, когда чуть больше узнаю про многопоточность. Спасибо!

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

В учебнике именно так. Технически, оно работает - инт приводится к указателю в этом месте, чтобы быть правильным параметром для pthread_create и для функции, с которой начинает работать поток. Потом, в функции print_hello_world, он приводится обратно к типу инт. Хотя, компилятор на такое крепко выругался. А как, с учётом требования на тип аргумента void*, передать i по значению, кроме как таким варварским способом?

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

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

Или у нас разные pthread?

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

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

У меня прототип pthread_create такой же. В учебнике (скрин: http://i.imgur.com/Xld2rZ7.png) автор, видимо, захотел передать по значению, и поэтому таким образом считерил, чтобы этот аргумент прошёл. Я правильно понимаю, что в обычной ситуации я не должен хотеть передавать такие аргументы по значению, и следует просто передавать указатель?

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

присутствуют две строки

Ты завершаешь основную нить, не дожидаясь завершения дочерних. Не думаю, что поведение стандартной библиотеки определено для такого случая. Добавь вызовы pthread_join().

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

Я правильно понимаю, что в обычной ситуации я не должен хотеть передавать такие аргументы по значению, и следует просто передавать указатель?

Думаю, какие-нибудь проблемы ты можешь поиметь.

6.3.2.3 Pointers:

5 — An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.

6 — Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.

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

А если в main заменить exit на pthread_exit, это будет корректно? Я так понял, что завершение процесса произойдёт по exit (в любом потоке), по выходу из main (любым способом), или по завершении каждого потока.

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

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

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

Который сам по себе просто(на x86_64) long int.

Это совпадение. А если бы он double передавал так?

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

Я правильно понимаю, что в обычной ситуации я не должен хотеть передавать такие аргументы по значению, и следует просто передавать указатель?

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

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

Т.е. с pthread_exit вместо exit процесс завершится только при завершении всех нитей?

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

Понял, спасибо.

У меня такой вопрос ещё появился: в книге много говорится о pthread_yield. Переключение между потоками в процессе осуществляется каким-то готовым планировщиком, или его нужно реализовывать самостоятельно?

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

Планировщик в системе естественно уже есть.

А какого года твоя книжка? Пробил по manу - там пишут, что pthread_yield нестандартный. Вместо него нужно юзать sched_yield

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

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

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

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

Э. Таненбаум, Х. Бос, «Современные операционные системы», 4-е издание, 2015 год.

Вероятно, информация о том, что планировщик уже есть, ждёт меня в следующих разделах. Текущий материал, похоже, рассказывает, «как может быть вообще».

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

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

То, что аргумент в стеке и то, что он изменяется это разные свойства.

С другой стороны ты навёл меня на мысль. Получается аргумент либо копировать до возсрата либо сразу формировать в куче.

Такой неявный стандарт. Представляю сколько человеко-часов отлавливать такие косяки.

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

Планировщик это планировщик ОС. В линуксах нет нитей - потоки это отдельные процессы.

Возможно на венде pthread работает как-то по другому.

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

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

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

Видимо, ты особо на Си не кодил

Царь, ты?

Если не нужно держать там сложные данные, и достаточно хранить целые числа, их хранят в виде указателей

Разницу между можно и нужно знаешь?

Весь этот выпендрёж ни к чему хорошему не приводит и запутывает программу. Из выражения Программа = Алгоритмы + Структуры данных пропадают структуры а алгоритмы бессмысленно запутываются.

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

Весь этот выпендрёж ни к чему хорошему не приводит и запутывает программу.

А людям ­— норм. По крайней мере, я такое в коде часто видел. Проблем с чтением не вызывает. Скорее наоборот, обёрнутый в коробочку int это дополнительные трудности при чтении и лишняя нагрузка на аллокатор.

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

Если у тебя один такой вызов и действительно инт, то нормально. Но если их много, то запутывание неизбежно.

Тот же com в винде или rpc в линуксе. Представь, что там через один лепят инты в указатели! Вот это будет трындец.

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

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

Если ты по сети посылаешь указатели, у меня для тебя плохие новости. Это уже «трындец».

i-rinat ★★★★★
()

Хм. Может я щас конечно глупость скажу - я с pthreads не работал, а с кутешными потоками. Но - ты создаешь потоки, но я не вижу чтобы тут был какой-то код который бы отвечал за то, что нужно дождаться когда они все отработают.

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

По сети идут данные. А программа отправляет указатели. man xdr, например.

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

Да, мне выше по тексту пояснили, что проблема в этом :)

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

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

Iron_Bug ★★★★★
()

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

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

да, скорее всего, это. определены стандарты для вызова exit. но всё равно никак не объяснить двойной вывод строки. exit сбрасывает буферы io, но поток при этом убивается. каким образом один и тот же буфер вылезает в stdout дважды - это загадка.

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

ну, это конкретно в glibc. кстати, можно проверить у меня, как musl к этому относится. по стандарту поток должен умереть. он не может «с того света» посылать IO.
впрочем, я так никогда не делала, всегда явно вызывала join дочерних потоков и только потом убивала все ресурсы. эксперименты с UB меня мало интересовали.

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

но это работает

ТС собственно рапортует, что это не работает.

Организовать структура и нормально передавать аргумент - 3-4 строчки. Передавать костыльно аргумент - ярчайший пример ложной оптимизации.

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

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

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

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

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

ты хочешь смотреть генерированный код библиотеки libc

Я с самого начала против этих передач int вместо нормальной структуры. Так что я не только не хочу, но и не столкнулся бы с такой фигней.

Но раз тут все хотят поломать голову на пустом месте, то смотреть надо что генерирует компилятор. Ключ -S.

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

передаются несколько байт. тебе какая разница, как их обозвать: int или void*? это как раз чистая формальность и на большинстве платформ будет работать. проблема вовсе не в этом.

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

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

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

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

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

проблема вовсе не в этом

Типичное «У меня всё работает» (с). Ты реально где-то кодишь с таким подходом? Грубое нарушение спецификации раз, нежелание разобраться двас, упорное отстаивание позиции с игнорированием аргументов трис. Щас окажется, что в ассемблерном коде ТС вообще нет i из-за оптимизации и привет. Что тогда туда передаётся?

anonymous
()

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

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