В статье авторы рассматривают многопоточное программирование.
Ключевые слова: поток, параллельное программирование, модель памяти, приложение.
В своё время параллельное программирование было интересно только для тех людей, которых привлекали сложные задачи для больших суперкомпьютеров. В настоящее время, когда на многоядерных процессорах начали работать обычные приложения, параллельное программирование быстро стало технологией, которая заинтересовала основную массу программистов, а также параллелизм в целом и многопоточное программирование, которое должен освоить и уметь применять любой конкурентоспособный профессиональный разработчик программного обеспечения.
Таким образом, можно выделить всего лишь две ключевые причины употребления параллелизма в приложениях: повышение производительности и разделение обязанностей. Поэтому если объединить взаимосвязанные части кода, а другие разъединить, то программа станет понятнее, будет проще тестироваться, а также уменьшится количество ошибок. Вычислительная мощь увеличивается, когда задачи выполняются параллельно.
Определим основные понятия. Параллелизм — это выполнение системой некоторых действий в одно время. Поток — это часть процесса, которая выполняет определенные задачи. Многопоточность — это свойство приложения, которое состоит в том, что процесс состоит из нескольких параллельно выполняющихся потоков.
Рассмотрим параллелизм на примере компилируемого языка программирования С++. Данный язык является популярным и широко используемым при создании серверов, драйверов устройств, операционных систем, различных прикладных программ, игр и приложений.
Расширения стандарта С++ 1998 не поддерживали корректную работу потоков, поэтому писать многопоточные приложения не получалось. Тогда был запущен стандарт C++11. Помимо того, что в нем определена абсолютно новая модель памяти с поддержкой многопоточности, так еще и в стандартную библиотеку C++ интегрированы классы для защиты разделяемых данных, управления потоками, синхронизации операций между потоками и низкоуровневых атомарных операций [1].
Эффективность — одна из проблем, с которой могли столкнуться разработчики приложений на С++. Приходится «платить за абстрагирование» при использовании каких-либо высокоуровневых механизмов, а не низкоуровневых средств, обертываемых ими. Тогда для того чтобы код стал более удобным для сопровождения помимо новой модели памяти была включена полная библиотека атомарных операций для прямого управления на уровне битов и байтов, а также средства межпоточной синхронизации и обеспечения видимости любых изменений [1].
Существуют различные библиотеки шаблонов, с помощью которых можно упростить реализацию многопоточности. Например, Intel Threading Building Blocks, Threads, OpenMP, Boost, а также Pthread.
Рассмотрим первую из них — Intel Treading Building Blocks. Она может работать с потоками, определяя их оптимальное количество, которое равняется количеству ядер процессора. Такая библиотека является достаточно функциональной, поскольку содержит в себе кроме функций для распараллеливания цикла ещё и сортировку (parallel_sort), объекты синхронизации, контейнеры, планировщика задач, аллокаторы.
Вторая — Threads. Эта библиотека появилась в стандарте языка в 2011 году. В ней представлено множество инструментов для работы с потоками, такие как условные переменные, мьютексы и т. д.
Третья — OpenMP. Основной упор в OpenMP делается на распараллеливание циклов.
Библиотека Boost — это набор частично компилируемых исходных кодов.
Pthreads включает в себя все, что нужно для создания многопоточных приложений. Сначала нужно создать идентификатор потока — его “имя”: pthread_t имя_потока. Функция pthread_create() позволяет добавить новый поток к текущему процессу, то есть создать его.
Продемонстрируем функцию в упрощенном виде на рис. 1:
Рис. 1. Создание потока
В случае успеха функция возвращает 0, в противном случае — ненулевое значение.
thread — это идентификатор созданного потока (указатель на область памяти, в которой в случае успешного создания потока размещается объект типа pthread_t, идентифицирующий поток), attr — атрибуты (указатель напеременную типа pthread_attr_t), которые получит поток; если указано NULL, то поток получит атрибуты по умолчанию: неограниченный, присоединяемый, имеющий приоритет родителя и размер стека по умолчанию. start_routine — функция, которую будет исполнять создаваемый поток; arg — единственный аргумент, который будет передан в функцию start_routine. Если в аргументе нет необходимости, необходимо указывать NULL.
Когда потоки принадлежат единственному процессу, они имеют общее адресное пространство. То есть, если для процесса определены глобальные переменные, то всякий поток может изменить их, а также иметь к ним доступ. Бывает так, что один поток читает данные в то время, когда другой поток их записывает. С помощью синхронизации можно исключить такие случаи. Кроме того, синхронизация позволяет управлять порядком исполнения потоков, что может быть полезным, например, если одному потоку для продолжения работы необходимы данные от другого потока. В Pthreads есть несколько способов синхронизации потоков. К наиболее часто используемым относятся мьютексы, барьеры, спинлоки и условные переменные.
Когда программируется приложение с несколькими потоками, обеспечивается thread safety — потоковая безопасность для функций . Приложениям, выполняющимся через множество процессов, не доступны такие требования.
Таким образом, применение Pthreads не гарантирует лучшую производительность, но с использованием примитивов синхронизации можно добиться наилучшего результата при параллельном программировании.
Литература:
1. Уильямс Э. Параллельное программирование на С++ в действии. Практика разработки многопоточных программ. — Москва: ДМК Пресс, 2012.
2. C++, [Электронный ресурс], Режим доступа: https://ru.wikipedia.org/wiki/C %2B %2B/, дата обращения: 09.05.2022
3. Pthreads: Потоки в русле POSIX++, [Электронный ресурс], Режим доступа: https://habr.com/ru/post/326138/, дата обращения: 09.05.2022