позднее он в цикле инициализируется объектом (тоже в main) >> arg = new DATA;
но удаляется объект почемуто из функций потоков >> delete a; // удаляем свои данные
причем удаление происходит После разблокирования мутекса!
Так делать очень опасно! можете легко прибить чужой объект или вычитать чужие данные (чтение тоже выполняется вне лока мутекса почемуто)
Безопасные варианты: 1) создать массив указателей DATA *arg[SIZE_I][SIZE_J] и отдавать каждому потоку указатель на индивидуальный кусок памяти. 2)выполнять все операции с общей памятью только при заблокированном мутексе (тоесть в потоках чтение, модификация и освобождение общей памяти должно быть внутри общего pthread_mutex_lock(&lock)). 3) удалять объекты только в том потоке который их создал.
Код статьи в качестве примера использовать не рекомендуется ибо в нем автор наступил на те грабли от которых по идее должен уберечь читателей.
3, Romzzzec ( ? ), 12:48, 14/09/2010 [ответить]
+ / –
>>//создаем поток для ввода >>pthread_create(&thr[i+z], NULL, input_thr, (void *)arg);
как я понял должны создаваться потоки для ввода каждого элемента матрицы, но i=1,z=2 — &thr[i+z] укажет на &thr[3] i=2,z=1 — &thr[i+z] укажет на &thr[3]
>>//Ожидаем завершения всех потоков >>//идентификаторы потоков хранятся в массиве >>pthread_join(thr[i], NULL);
при i=3,z=3 — &thr[i+z] укажет на &thr[6] и этот поток мы не ждем, т.к. size_i=4, правильно? соответственно и расчет данных для таких элементов будет «левым»?
Источник
Потоки
Хочется наконец-то выполнить данное мною еще полгода назад обещание и рассказать о том, что такое потоки в Unix и каких типов они бывают. Предполагается, что читатель хотя бы в общих чертах представляет себе что такое процесс и поток.
В Unix изначально существовала только одна модель одновременного выполнения нескольких задач — разделение их на несколько процессов, каждому из которых выделялось бы некоторое количество процессорного времени. Изначально эта схема была чрезвычайно проста и эффективна, в частности из-за того, что Unix в своем первоначальном варианте был рассчитан на работу только с одним процессором. Это значительно упрощало всю схему обеспечения многозадачности, потому что при ориентированности на один процессор синхронизация в пределах ядра не требовалась (учитывая то, что вызовы ядра всегда минимизировались по времени и не могли вытесняться другими вызовами ядра).
═
══
Лирическое отступление:
═
Распространено заблуждение, что «свободные» варианты Unix ни в чем не уступают коммерческим и что единственный плюс, который можно получить приобретая коммерческий вариант Unix, это техническая поддержка со стороны фирмы-производителя. Так вот это не так: коммерческие операционные системы отличаются во многом, в частности, в алгоритмах, поддерживающих многозадачность, методах распределения оперативной памяти или сетевых средствах. Другое дело, что в большинстве ситуаций возможностей той же FreeBSD хватает с избытком.
С другой стороны, именно развитие этого подхода в разных направлениях определило множество сложных коммерческих и свободно-распространяемых операционных систем, которые имеются на сегодняшний момент.
Фактически, с процессом связывается его адресное пространство (данные), код выполняемой программы, стек, содержимое регистров и еще некоторое количество структур данных ядра (таких как идентификаторы группы и пользователя, присоединенный терминал, приоритет выполнения и прочее). В тот момент, когда происходит переключение процессора с одного процесса на другой, ядро должно каким-то образом сохранить состояние одного процесса и потом восстановить состояние другого. Мало того, когда один процесс вызывает какой-то вызов ядра, выполняются примерно такие же действия: сохраняется состояние процесса, затем происходит переключение регистров таким образом, чтобы было возможно выполнить код, находящийся внутри ядра, после чего состояние процесса (обращавшегося к ядру или иного, если время выделенное процессу уже истекло), восстанавливается.
Эта схема очень проста, но у нее есть много недостатков. Например, значительно усложняется межпроцессное взаимодействие. Конечно же, в современных вариантах Unix существуют такие способы, как разделяемые пространства оперативной памяти, но было время, когда единственным способом оперативно передать большое количество данных от одного процесса к другому было использование программного канала. В то же время, очень часто является удобным разделить одну и ту же задачу на несколько маленьких подзадач так, чтобы они разделяли все свои данные.
Кроме того, описанный выше порядок смены контекста процесса слишком дорогостоящий и не позволяет эффективно разбить один поток управления на несколько, потому что при этом возникают накладные расходы на сохранение и восстановление состояний процесса.
Потоки появились как логическое продолжение понятия процесса. Во-первых, появились некоторые задачи внутри ядра, которые точно так же требовалось выполнять параллельно с остальными работающими процессами. Эти задачи не имеют своего собственного адресного пространства и работают внутри пространства ядра. Фактически, они представлены лишь стеком данных и набором регистров, что позволяет ядру очень быстро переключаться между ними. Понятно, что ядро современной операционной системы посредством разделения некоторых частей внутри себя на такие потоки управления, может работать одновременно на нескольких процессорах, в случае необходимости.
Подобные потоки (своеобразные внутренние процессы ядра) называются потоками ядра. В современных операционных системах традиционные процессы реализуются именно на них.
Потоки внутри пользовательской программы обычно служат для того, чтобы дать возможность программисту разделить задачу на несколько подзадач для более удобного и логичного внутреннего представления программы. Обычно программист не особенно заботится о том, сколько процессоров есть на компьютере, где будет запущена его программа, и действительно ли затребованные им десять потоков будут работать одновременно на десяти процессорах. Поэтому потоки, предоставляемые в распоряжение клиентских программ, на самом деле, могут вообще не иметь никакого отношения к потокам ядра и быть ограничены только одним процессором!
Мало того, переключение потоков может быть реализовано и без поддержки потоков ядром исключительно на пользовательском уровне. То есть, если знать о том, как на конкретной архитектуре микропроцессора устроен стек, то, опять же, имея доступ к регистрам процессора, можно переключать выполнение команд из одного потока в другой, сохраняя при этом всю «историю» вызова подпрограмм в каждом потоке. Тем самым получается многопоточная программа, не требующая поддержки потоков со стороны ядра. Вообще, ядро в этом случае может быть честно уверенно, что выполняется обычный процесс — с точки зрения операционной системы подобный многопоточный процесс ничем не отличается от однопоточного.
Такой подход обеспечения многопоточности получил название «пользовательских потоков» и он имеет некоторые преимущества по сравнению с использованием нескольких процессов: например, совершенно не требует сохранения всего состояния процесса при переключении от одного потока к другому, только модификацию нескольких регистров (командного и стека). К недостаткам можно отнести то, что даже на нескольких процессорах такие потоки не могут выйти за пределы одного, то есть они могут вместе с процессом переходить с одного процессора на другой, но не могут выполняться одновременно на двух.
В некоторых операционных системах ядро все-таки предоставляет доступ пользовательским процессам к потокам ядра. При этом, понятно, что такие потоки требуют значительно меньше затрат на переключение, чем процессы и позволяют при этом использовать столько процессоров, сколько имеется на данном компьютере, но все операции с потоками требуют переключения контекста процесса в контекст ядра, включая такие часто используемые процедуры в многопоточных приложениях, как синхронизацию. Это значит, что эффективно их можно использовать только в случаях практически независимых потоков, которые очень редко используют общие переменные.
Существуют комбинированные подходы. При этом ядро операционной системы все-таки знает о том, сколько потоков имеется у процесса и может выделить несколько потоков ядра на один процесс так, чтобы пользовательские потоки можно было распределить между потоками ядра. При этом, понятно, потоков ядра может быть выделено много меньше, чем пользовательских потоков.
Практически все известные коммерческие Unix-подобные операционные системы поддерживают такие «комбинированные» потоки. Мало того, почти выработан общий стандарт на программный интерфейс к пользовательским потокам под названием pthreads, используя который программисты могут писать многопоточные приложения почти не задумываясь о том, под какой операционной системой будут выполняться их программы.
«Почти», это потому, что существует много тонкостей. Например, в известной операционной системе FreeBSD библиотека потоков pthreads является целиком пользовательской (то есть, несколько потоков будут всегда выполняться в пределах одного процессора). Связано это с несколькими ограничениями внутри ядра, которые не позволяют сделать целиком совместимую со стандартом библиотеку pthreads, использующую процессы ядра. Если об этих ограничениях знать, то под FreeBSD можно воспользоваться «портом» linuxthreads.
Использование процессов ядра зачастую очень полезно. Например, если существует цикл вида:
где A[i], B[i] и C[i] — квадратные матрицы достаточно большого размера, то разделив этот цикл на, допустим, имеющиеся в наличии четыре процессора функциями вида:
где через arg передается число от 0 до 3, то можно получить ускорение данного куска программного кода в четыре раза (если, конечно же, операция умножения матриц может выполняться параллельно).
С другой стороны, если просто взять такой код и перенести его на компьютер с, допустим, двумя процессорами, то он будет выполняться медленнее, чем если бы использовалось только два потока ядра, потому что много времени уйдет на переключение из одного потока в другой.
Кстати сказать, некоторые современные коммерческие компиляторы фортрана умеют выделять подобные циклы и самостоятельно распараллеливать на заказанное при выполнении программы количество процессов ядра.
В то же самое время, использование в подобной ситуации пользовательских потоков будет совершенно неоправданно. Зато пользовательские потоки позволяют программисту достаточно простым способом организовывать ожидание некоторых редких событий, к примеру, нажатие пользователем клавиши на клавиатуре или получения данных из сетевого устройства. Использование потоков в этих случаях позволяет зачастую значительно упростить программу внутри.
Тем не менее, я не рекомендую использовать пользовательские потоки для программ, работающих под большими нагрузками. Опыт показывает, что библиотеки пользовательских потоков очень часто содержат в себе ошибки, а «реентерабельные» версии используемых подпрограмм значительно меньше оттестированы, чем обычные. Кроме того, становятся существенными затраты на переключение пользовательских потоков и, в общем не нужную синхронизацию. В то же время, обычно программу, использующую пользовательские потоки можно не менее эффективно переписать при помощи использования неблокирующих, асинхронных или событийных средств ввода-вывода, предоставляемых операционной системой.
Не стоит забывать и о традиционном для Unix’а разделения задач: процессах. Во многих ситуациях последовательной обработки данных их использование позволяет эффективно загрузить все используемые ресурсы ЭВМ не сталкиваясь при этом с потенциально опасными «реентерабельными» версиями программных библиотек.
Резюме
Необходимо понимать различие между потоками ядра и пользовательскими потоками. Они отличаются друг от друга и применяются в совершенно разных ситуациях. Одно из основных отличий: потоки ядра могут работать одновременно на нескольких процессорах, а пользовательские — нет.
Источник
Что такое поток выполнения linux
Многопоточность является естественным продолжением многозадачности, точно также как виртуальные машины, позволяющие запускать несколько ОС на одном компьютере, представляют собой логическое развитие концепции разделения ресурсов. В рамках неформального, но простого, определения, поток — это выполнение последовательности машинных инструкций. В многопоточном приложении одновременно работает несколько потоков. Некоторые авторы избегают термина «поток» и используют вместо него термин «нить», вероятно для того, чтобы потоки программ не путались с потоками ввода-вывода. Для обозначения последовательного выполнения цепочки инструкций мне лично больше нравится термин «поток», которым я и буду пользоваться. Надеюсь, читатели Linux Format не запутаются в контекстах и, встретив слово поток, всегда поймут, идет ли речь о потоках программы, потоках ввода вывода, или о бурных паводковых потоках.
Прежде чем приступать к программированию потоков, следует ответить на вопрос, а нужны ли они вам. Мы уже знаем, насколько хорошо развиты в Linux средства межпроцессного взаимодействия. С помощью управления процессами в Linux можно решить многие задачи, которые в других ОС решаются только с помощью потоков. Потоки часто становятся источниками программных ошибок особого рода. Эти ошибки возникают при использовании потоками разделяемых ресурсов системы (например, общего адресного пространства) и являются частным случаем более широкого класса ошибок – ошибок синхронизации. Если задача разделена между независимыми процессами, то доступом к их общим ресурсам управляет операционная система, и вероятность ошибок из-за конфликтов доступа снижается. Впрочем, разделение задачи между несколькими независимыми процессами само по себе не защитит вас от других разновидностей ошибок синхронизации. В пользу потоков можно указать то, что накладные расходы на создание нового потока в многопоточном приложении ниже, чем накладные расходы на создание нового самостоятельного процесса. Уровень контроля над потоками в многопоточном приложении выше, чем уровень контроля приложения над дочерними процессами. Кроме того, многопоточные программы не склонны оставлять за собой вереницы зомби или «осиротевших» независимых процессов.
Первая подсистема потоков в Linux появилась около 1996 года и называлась, без лишних затей, – LinuxThreads. Рудимент этой подсистемы, который вы найдете в любой современной системе Linux, – файл /usr/include/pthread.h, указывает год релиза – 1996 и имя разработчика – Ксавье Лерой (Xavier Leroy). Библиотека LinuxThreads была попыткой организовать поддержку потоков в Linux в то время, когда ядро системы еще не предоставляло никаких специальных механизмов для работы с потоками. Позднее разработку потоков для Linux вели сразу две конкурирующие группы – NGPT и NPTL. В 2002 году группа NGPT фактически присоединилась к NPTL и теперь реализация потоков NPTL является стандартом Linux. Подсистема потоков Linux стремится соответствовать требованиям стандартов POSIX, так что новые многопоточные приложения Linux должны без проблем компилироваться на новых POSIX-совместимых системах.
Потоки и процессы
Интересно рассмотреть механизм, с помощью которого Linux решает проблему идентификаторов процессов потоков. В Linux у каждого процесса есть идентификатор. Есть он, естественно, и у процессов-потоков. С другой стороны, спецификация POSIX 1003.1c требует, чтобы все потоки многопоточного приложения имели один идентификатор. Вызвано это требование тем, что для многих функций системы многопоточное приложение должно представляться как один процесс с одним идентификатором. Проблема единого идентификатора решается в Linux весьма элегантно. Процессы многопоточного приложения группируются в группы потоков (thread groups). Группе присваивается идентификатор, соответствующий идентификатору первого процесса многопоточного приложения. Именно этот идентификатор группы потоков используется при «общении» с многопоточным приложением. Функция getpid(2), возвращает значение идентификатора группы потока, независимо от того, из какого потока она вызвана. Функции kill() waitpid() и им подобные по умолчанию также используют идентификаторы групп потоков, а не отдельных процессов. Вам вряд ли понадобится узнавать собственный идентификатор процесса-потока, но если вы захотите это сделать, вам придется воспользоваться довольно экзотичной конструкцией. Получить идентификатор потока (thread ID) можно с помощью функции gettid(2), однако саму функцию нужно еще определить с помощью макроса _syscall. Работа с функцией gettid() выглядит примерно так:
Более подробную информацию вы можете получить на страницах man, посвященных gettid() и _syscall. Потоки создаются функцией pthread_create(3), определенной в заголовочном файле
. Первый параметр этой функции представляет собой указатель на переменную типа pthread_t, которая служит идентификатором создаваемого потока. Второй параметр, указатель на переменную типа pthread_attr_t, используется для передачи атрибутов потока. Третьим параметром функции pthread_create() должен быть адрес функции потока. Эта функция играет для потока ту же роль, что функция main() – для главной программы. Четвертый параметр функции pthread_create() имеет тип void *. Этот параметр может использоваться для передачи значения, возвращаемого функцией потока. Вскоре после вызова pthread_create() функция потока будет запущена на выполнение параллельно с другими потоками программы. Таким образом, собственно, и создается новый поток. Я говорю, что новый поток запускается «вскоре» после вызова pthread_create() потому, что перед тем как запустить новую функцию потока, нужно выполнить некоторые подготовительные действия, а поток-родитель между тем продолжает выполняться. Непонимание этого факта может привести вас к ошибкам, которые трудно будет обнаружить. Если в ходе создания потока возникла ошибка, функция pthread_create() возвращает ненулевое значение, соответствующее номеру ошибки.
Функция потока должна иметь заголовок вида:
Имя функции, естественно, может быть любым. Аргумент arg, — это тот самый указатель, который передается в последнем параметре функции pthread_create(). Функция потока может вернуть значение, которое затем будет проанализировано заинтересованным потоком, но это не обязательно. Завершение функции потока происходит если:
функция потока вызвала функцию pthread_exit(3);
функция потока достигла точки выхода;
поток был досрочно завершен другим потоком.
Функция pthread_exit() представляет собой потоковый аналог функции _exit(). Аргумент функции pthread_exit(), значение типа void *, становится возвращаемым значением функции потока. Как (и кому?) функция потока может вернуть значение, если она не вызывается из программы явным образом? Для того, чтобы получить значение, возвращенное функцией потока, нужно воспользоваться функцией pthread_join(3). У этой функции два параметра. Первый параметр pthread_join(), – это идентификатор потока, второй параметр имеет тип «указатель на нетипизированный указатель». В этом параметре функция pthread_join() возвращает значение, возвращенное функцией потока. Конечно, в многопоточном приложении есть и более простые способы организовать передачу данных между потоками. Основная задача функции pthread_join() заключается, однако, в синхронизации потоков. Вызов функции pthread_join() приостанавливает выполнение вызвавшего ее потока до тех пор, пока поток, чей идентификатор передан функции в качестве аргумента, не завершит свою работу. Если в момент вызова pthread_join() ожидаемый поток уже завершился, функция вернет управление немедленно. Функцию pthread_join() можно рассматривать как эквивалент waitpid(2) для потоков. Эта функция позволяет вызвавшему ее потоку дождаться завершения работы другого потока. Попытка выполнить более одного вызова pthread_join() (из разных потоков) для одного и того же потока приведет к ошибке.
Посмотрим, как все это работает на практике. Ниже приводится фрагмент листинга программы threads, (полный текст программы вы найдете в исходниках программы в файле threads.c).
Рассмотрим сначала функцию thread_func(). Как вы, конечно, догадались, это и есть функция потока. Наша функция потока очень проста. В качестве аргумента ей передается указатель на переменную типа int, в которой содержится номер потока. Функция потока распечатывает этот номер несколько раз с интервалом в одну секунду и завершает свою работу. В функции main() вы видите две переменных типа pthread_t. Мы собираемся создать два потока и у каждого из них должен быть свой идентификатор. Вы также видите две переменные типа int, id1 и id2, которые используются для передачи функциям потоков их номеров. Сами потоки создаются с помощью функции pthread_create().В этом примере мы не модифицируем атрибуты потоков, поэтому во втором параметре в обоих случаях передаем NULL. Вызывая pthread_create() дважды, мы оба раза передаем в качестве третьего параметра адрес функции thread_func, в результате чего два созданных потока будут выполнять одну и ту же функцию. Функция, вызываемая из нескольких потоков одновременно, должна обладать свойством реентерабельности (этим же свойством должны обладать функции, допускающие рекурсию). Реентерабельная функция, это функция, которая может быть вызвана повторно, в то время, когда она уже вызвана (отсюда и происходит ее название). Реентерабельные функции используют локальные переменные (и локально выделенную память) в тех случаях, когда их не-реентерабельные аналоги могут воспользоваться глобальными переменными.
Мы вызываем последовательно две функции pthread_join() для того, чтобы дождаться завершения обоих потоков. Если мы хотим дождаться завершения всех потоков, порядок вызова функций pthread_join() для разных потоков, очевидно, не имеет значения.
Для того, чтобы скомпилировать программу threads.c, необходимо дать команду:
Команда компиляции включает макрос _REENTERANT. Этот макрос указывает, что вместо обычных функций стандартной библиотеки к программе должны быть подключены их реентерабельные аналоги. Реентерабельный вариант библиотеки glibc написан таким образом, что вы, скорее всего, вообще не обнаружите никаких различий в работе с реентерабельными функциями по сравнению с их обычными аналогами. Мы указываем компилятору путь для поиска заголовочных файлов и путь для поиска библиотек /usr/include/nptl и /usr/lib/nptl соответственно. Наконец, мы указываем компоновщику, что программа должна быть связана с библиотекой libpthread, которая содержит все специальные функции, необходимые для работы с потоками.
У вас, возможно, возникает вопрос, зачем мы использовали две разные переменные, id1 и id2, для передачи значений двум потокам? Почему нельзя использовать одну переменную, скажем id, для обоих потоков? Рассмотрим такой фрагмент кода:
Конечно, в этом случае оба потока получат указатель на одну и ту же переменную, но ведь значение этой переменной нужно каждому потоку только в самом начале его работы. После того, как поток присвоит это значение своей локальной переменной loc_id, ничто не мешает нам использовать ту же переменную id для другого потока. Все это верно, но проблема заключается в том, что мы не знаем, когда первый поток начнет свою работу. То, что функция pthread_create() вернула управление, не гарантирует нам, что поток уже выполняется. Вполне может случиться так, что первый поток будет запущен уже после того, как переменной id будет присвоено значение 2. Тогда оба потока получат одно и то же значение id. Впрочем, мы можем использовать одну и ту же переменную для передачи данных функциям потока, если воспользуемся средствами синхронизации. Этим средствам будет посвящена следующая статья.
Досрочное завершение потока
Чаще всего функция pthread_setcancelstate() используется для временного запрета завершения потока. Допустим, мы программируем поток, и знаем, что при определенных условиях программа может потребовать его досрочного завершения. Но в нашем потоке есть участок кода, во время выполнения которого завершать поток крайне нежелательно. Мы можем оградить этот участок кода от досрочного завершения с помощью пары вызовов pthread_setcancelstate():
Первый вызов pthread_setcancelstate() запрещает досрочное завершение потока, второй – разрешает. Если запрос на досрочное завершение потока поступит в тот момент, когда поток игнорирует эти запросы, выполнение запроса будет отложено до тех пор, пока функция pthread_setcancelstate() не будет вызвана с аргументом PTHREAD_CANCEL_ENABLE. Что именно произойдет дальше, зависит от более тонких настроек потока. Рассмотрим пример программы (на диске вы найдете ее в файле canceltest.c)
В самом начале функции потока thread_func() мы запрещаем досрочное завершение потока, затем выводим четыре тестовых сообщения с интервалом в одну секунду, после чего разрешаем досрочное завершение. Далее, с помощью функции pthread_testcancel(), мы создаем точку отмены (cancellation point) потока. Если досрочное завершение потока было затребовано, в этот момент поток должен завершиться. Затем мы выводим еще одно диагностическое сообщение, которое пользователь не должен видеть, если программа сработает правильно.
В главной функции программы мы создаем поток, затем дожидаемся, пока значение глобальной переменной i станет больше нуля (это гарантирует нам, что поток уже запретил досрочное завершение) и вызываем функцию pthread_cancel(). После этого мы переходим к ожиданию завершения потока с помощью pthread_join(). Если вы скомпилируете и запустите программу, то увидите, что поток распечатает четыре тестовых сообщения I’m still running! (после первого сообщения главная функция программы выдаст запрос на завершение потока).
Поскольку поток завершится досрочно, последнего тестового сообщения вы не увидите. Интересна роль функции pthread_testcancel(). Как уже отмечалось, эта функция создает точку отмены потока. Зачем нужны особые точки отмены? Дело в том, что даже если досрочное завершение разрешено, поток, получивший запрос на досрочное завершение, может завершить работу не сразу. Если поток находится в режиме отложенного досрочного завершения (именно этот режим установлен по умолчанию), он выполнит запрос на досрочное завершение, только достигнув одной из точек отмены. В соответствии со стандартом POSIX, точками отмены являются вызовы многих «обычных» функций, например open(), pause() и write(). Про функцию printf() в документации сказано, что она может быть точкой отмены, но в Linux при попытке остановиться на printf() происходит нечто странное – поток завершается, но pthread_join() не возвращает управления. Поэтому мы создаем явную точку отмены с помощью вызова pthread_testcancel().
Впрочем, мы можем выполнить досрочное завершение потока, не дожидаясь точек останова. Для этого необходимо перевести поток в режим немедленного завершения, что делается с помощью вызова pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); В этом случае беспокоиться о точках останова уже не нужно. Вызов pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); снова переводит поток в режим отложенного досрочного завершения.
Тема потоков практически неисчерпаема (простите за каламбур), но мы посвятим потокам только лишь еще одну статью, в которой рассмотрим вопросы синхронизации и атрибуты потоков.