Многопоточное программирование

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 20:20, 6 июня 2018.

Многопоточность — свойство платформы (например, операционная система, виртуальная машина и т. д.) или прикладное программное обеспечение/приложения, состоящее в том, что процесс, порождённый в операционной системе, может состоять из нескольких потоков, выполняющих параллельные вычисления, то есть без предписанного порядка во времени. При выполнении некоторых задач такое разделение может достичь более эффективного использования ресурсов вычислительной машины.[Источник 1]

Описание

Сутью многопоточности является квазимногозадачность на уровне одного исполняемого процесса, то есть все потоки выполняются в адресном пространстве процесса. Кроме этого, все потоки процесса имеют не только общее адресное пространство, но и общие Файловый дескриптор (дескрипторы файлов). Выполняющийся процесс имеет как минимум один (главный) поток.

Многопоточность не следует путать ни с многозадачностью, ни с многопроцессорностью, несмотря на то, что операционная система (операционные системы), реализующая многозадачность, как правило, реализует и многопоточность.

К достоинствам многопоточной реализации той или иной системы перед многозадачной можно отнести следующее:

  • Упрощение программы в некоторых случаях за счёт использования общего адресного пространства.
  • Меньшие относительно процесса временны́е затраты на создание потока.

К достоинствам многопоточной реализации той или иной системы перед однопоточной можно отнести следующее:

  • Упрощение программы в некоторых случаях, за счёт вынесения механизмов чередования выполнения различных слабо взаимосвязанных подзадач, требующих одновременного выполнения, в отдельную подсистему многопоточности.
  • Повышение производительности процесса за счёт распараллеливания процессорных вычислений и операций ввода-вывода.

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

Многопотоковое программирование предложено в качестве средства разработки параллельных программ для многопроцессорных систем (систем с разделяемой памятью). При этом реальное разнесение потоков управления на разные процессоры - задача ОС. Фирма SUN Microsystems для поддержки потоков (нитей) управления реализовала легковесные процессы LWP (LightWeight Processes). Диспетчирование LWP - практически не управляемая пользователем процедура. Потоки характеризуются следующими атрибутами:

  • идентификатор потока (уникален в рамках процесса);
  • значение приоритета;
  • сигнальная маска.

Предложены 2 API потокового программирования:

  • фирмы SUN Microsystems (пионер в этом деле);
  • комитета POSIX.1C по стандартизации.

Здесь рассматривается вариант POSIX (Portable Operating System Interface for Unix). Все функции этого варианта имеют в своих именах префикс pthread_ и объявлены в заголовочном файле pthread.h.

Аппаратная реализация

На обычном процессоре управление потоками осуществляется операционной системой. Поток исполняется до тех пор, пока не произойдёт аппаратное прерывание, системный вызов или пока не истечёт отведённое для него операционной системой время. После этого процессор переключается на код операционной системы, который сохраняет состояние потока (его контекст) или переключается на состояние другого потока, которому тоже выделяется время на исполнение. При такой многопоточности достаточно большое количество тактов процессора тратится на код операционной системы, переключающий контексты. Если поддержку потоков реализовать аппаратно, то процессор сам сможет переключаться между потоками, а в идеальном случае - выполнять несколько потоков одновременно за каждый такт. Для операционной системы и пользователя один такой физический процессор будет виден как несколько логических процессоров.

Различают две формы многопоточности, которые могут быть реализованы в процессорах аппаратно:

  • Временная многопоточность(англ. Temporal multithreading).
  • Одновременная многопоточность (англ. Simultaneous multithreading).

Типы реализации потоков

  • Поток в пространстве пользователя. Каждый процесс имеет таблицу потоков, аналогичную таблице процессов ядра. К недостаткам можно отнести:
  1. Отсутствие прерывания по таймеру внутри одного процесса
  2. При использовании блокирующего системного запроса для процесса все его потоки блокируются.
  3. Сложность реализации
  • Поток в пространстве ядра. Наряду с таблицей процессов в пространстве ядра имеется таблица потоков.
  • «Волокна» (англ. fibers). Несколько потоков режима пользователя, исполняющихся в одном потоке режима ядра. Поток пространства ядра потребляет заметные ресурсы, в первую очередь физическую память и диапазон адресов режима ядра для стека режима ядра. Поэтому было введено понятие «волокна» — облегчённого потока, выполняемого исключительно в режиме пользователя. У каждого потока может быть несколько «волокон».[Источник 2]


Взаимодействие потоков

В многопоточной среде часто возникают задачи, требующие приостановки и возобновления работы одних потоков в зависимости от работы других. В частности это задачи, связанные с предотвращенем конфликтов доступа при использовании одних и тех же данных или устройств из параллельно исполняемых потоков. Для решения таких задач используются специальные объекты для взаимодействия потоков, такие как взаимоисключения (мьютексы), семафоры, критические секции, события и т.п. Многие из этих объектов являются объектами ядра и могут применяться не только между потоками одного процесса, но и для взаимодействия между потоками разных процессов.

  • Взаимоисключения (mutex, мьютекс) — это объект синхронизации, который устанавливается в особое сигнальное состояние, когда не занят каким-либо потоком. Только один поток владеет этим объектом в любой момент времени, отсюда и название таких объектов (от английского mutually exclusive access — взаимно исключающий доступ) — одновременный доступ к общему ресурсу исключается. После всех необходимых действий мьютекс освобождается, предоставляя другим потокам доступ к общему ресурсу. Объект может поддерживать рекурсивный захват второй раз тем же потоком, увеличивая счётчик, не блокируя поток, и требуя потом многократного освобождения. Такова, например, критическая секция в Win32. Тем не менее, есть и такие реализации, которые не поддерживают такое и приводят к Взаимная блокировка|взаимной блокировке потока при попытке рекурсивного захвата. Например, это FAST_MUTEX в ядре Windows.
  • Семафоры представляют собой доступные ресурсы, которые могут быть приобретены несколькими потоками в одно и то же время, пока пул ресурсов не опустеет. Тогда дополнительные потоки должны ждать, пока требуемое количество ресурсов не будет снова доступно.
  • ERESOURCE. Мьютекс, поддерживающий рекурсивный захват, с семантикой разделяемого или эксклюзивного захвата. Семантика: объект может быть либо свободен, либо захвачен произвольным числом потоков разделяемым образом, либо захвачен всего одним потоком эксклюзивным образом. Любые попытки осуществить захваты, нарушающее это правило, приводят к блокировке потока до тех пор, пока объект не освободится так, чтобы сделать захват разрешённым. Также есть операции вида TryToAcquire — никогда не блокирует поток, либо захватывает, либо (если нужна блокировка) возвращает FALSE, ничего не делая. Используется в ядре Windows, особенно в файловых системах — так, например, любому кем-то открытому дисковому файлу соответствует структура FCB, в которой есть 2 таких объекта для синхронизации доступа к размеру файла. Один из них — paging IO resource — захватывается эксклюзивно только в пути обрезания файла, и гарантирует, что в момент обрезания на файле нет активного ввода-вывода от кэша и от отображения в память.

Создание потока управления

 int pthread_create (pthread_t *tid_p, const pthread_attr_t *attr_p,
		void *(*func_p)(void *), void *arg_p)

Создает новый поток для функции, заданной параметром func_p. Эта функция имеет аргументом указатель (void *) и возвращает значение того же типа. Реально же в функцию передается аргумент arg_p. Идентификатор нового потока возвращается через tid_p.

Аргумент attr_p указывает на структуру, задающую атрибуты вновь создаваемого потока. Если attr_p=NULL, то используются атрибуты "по умолчанию" (но это плохая практика, т.к. в разных ОС эти значения могут быть различными, хотя декларируется обратное). Одна структура, указываемая attr_p, может использоваться для управления несколькими потоками.

Инициализация атрибутов потока

int pthread_attr_init (pthread_attr_t *attr_p)

Инициализирует структуру, указываемую attr_p, значениями "по умолчанию" (при этом распределяется кое-какая память). Атрибуты потока:

  • Область действия конкуренции (scope) [PTHREAD_SCOPE_PROCESS] - определяет связность потока с LWP.
  • Отсоединенность (detachstate) [PTHREAD_CREATE_JOINABLE] - определяет то, может или нет какой-либо другой поток ожидать окончания данного (посредством функции).
  • Адрес динамического стека потока (stackaddr) [NULL].
  • Размер динамического стека потока(stacksize) [1 Mb].
  • Приоритет потока (priority) [наследуется от потока-родителя].
  • Правила и параметры планирования. Неприятно то, что schedpolicy по умолчанию устанавливается в SCHED_OTHER, зависимую от ОС.

Освобождение памяти атрибутов потока

int pthread_attr_destroy (pthread_attr_t *attr_p)

Область конкуренции

int pthread_attr_setscope (pthread_attr_t *attr_p, int scope)
int pthread_attr_getscope (pthread_attr_t *attr_p, int *scope)

scope может принимать два значения: PTHREAD_SCOPE_PROCESS - для несвязанного потока; PTHREAD_SCOPE_SYSTEM - для связанного потока.

Состояние отсоединенности

int pthread_attr_setdetachstate (pthread_attr_t *attr_p, int detachstate)
int pthread_attr_getdetachstate (pthread_attr_t *attr_p, int *detachstate)

detachstate может принимать два значения: PTHREAD_CREATE_DETACHED - для отсоединеного потока; PTHREAD_CREATE_JOINABLE - для присоединенного потока.

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

Завершение потока

В потоках можно использовать стандартную функцию exit(), однако это ведет к немедленному завершению всех потоков и процесса в целом. Поток завершается вместе с вызовом return() в функции, вызванной pthread_create(). Поток заканчивает свое выполнение также с помощью функции

pthread_exit (void *status)

допустимо в качестве status использовать NULL. Поток может быть завершен другим потоком посредством функции pthread_cancel() (с этой функцией работают pthread_setcanceltype, pthread_setcancelstate и pthread_testcancel).

Ожидание завершения потока

int pthread_join (pthread_t tid, void **status)

Вызывающий поток блокируется до окончания потока с идентификатором tid. Поток с идентификатором tid не может быть отсоединенным

Получение идентификатора потока

pthread_t pthread_self (void)

Передача управления другому потоку

int sched_yield (void)

Передает управление другому потоку, имеющему приоритет равный или больший приоритета вызывающего потока.

Посылка сигнала потоку

int pthread_kill (pthread_t tid, int signum)

Посылает сигнал с идентификатором signum в поток, задаваемый идентификатором tid.

Манипулирование сигнальной маской потока

int pthread_sigmask (int mode, sigset_t *set_p, sigset_t *old_p)

Изменяет сигнальную маску потока в соответствии с аргументом mode, который может принимать следующие значения:

  • SIG_BLOCK - добавить сигналы из набора, указываемого set_p, в текущую сигнальную маску, описывающую блокируемые сигналы;
  • SIG_UNBLOCK - удалить сигналы, содержащиеся в наборе, указываемом set_p, из текущей сигнальной маски;
  • SIG_SETMASK - установить сигнальную маску, указываемую set_p, в качестве текущей.

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

Объекты синхронизации потоков управления

Потоки используют единое адресное пространство. Это означает, что все статические переменные доступны потокам в любой момент. Поэтому необходимы средства управления доступом к совместно используемым данным. Здесь возможно использование стандартных средств синхронизации различных процессов: каналы, очереди сообщений, межпроцессные семафоры. Однако, специально для межпотокового взаимодействия предложены индивидуальные средства:

  • взамоисключающие блокировки (mutex locks);
  • условные переменные (conditional variables);
  • семафоры (semaphores);
  • барьеры (barriers).

Указанные средства перечислены в порядке ухудшения их эффективности. Заметим, что доступ к атомарным данным (char, int, double) реализуется за один такт процессора, поэтому существуют ситуации (зависящие от логики программы), когда такие данные сами могут выступать в качестве средства синхронизации.

Взамоисключающие блокировки

int pthread_mutex_init (pthread_mutex_t *mp, const pthread_mutex_attr_t *mattrp)

инициализирует взаимоисключающую блокировку, выделяя необходимую память. Если mattrp=NULL, то создается блокировка с атрибутами "по умолчанию". В настоящее время атрибут один - область действия блокировки, его умолчательное значение - PTHREAD_PROCESS_PRIVATE (а может быть еще PTHREAD_PROCESS_SHARED).

int pthread_mutex_destroy (pthread_mutex_t *mp)

разрушает блокировку, освобождая выделенную память.

int pthread_mutex_lock (pthread_mutex_t *mp)
int pthread_mutex_unlock (pthread_mutex_t *mp)
int pthread_mutex_trylock (pthread_mutex_t *mp)

С помощью pthread_mutex_lock() поток пытается захватить блокировку. Если же блокировка уже принадлежит другому потоку, то вызывающий поток ставится в очередь (с учетом приоритетов потоков) к блокировке. После возврата из функции pthread_mutex_lock() блокировка будет принадлежать вызывающему потоку.

Функция pthread_mutex_unlock() освобождает захваченную ранее блокировку. Освободить блокировку может только ее владелец.

Функция pthread_mutex_trylock() - неблокирующая версия функции pthread_mutex_lock(). Если на момент обращения к этой функции блокировка уже захвачена, то происходит немедленный возврат из функции со значением EBUSY.

Условные переменные

Применяются в сочетании со взаимоис ключающими блокировками. Общая схема использования такова. Один поток устанавливает взаимоисключающую блокировку и затем блокирует себя по условной переменной (путем вызова функции pthread_cond_wait()), при этом автоматически (но временно) освобождается взаимоисключающая блокировка. Когда какой-либо другой поток посредством вызова функции pthread_cond_signal() сигнализирует по условной переменной, то первый поток разблокируется и ему возвращается во владение взаимоисключающая блокировка.

int pthread_cond_init (pthread_cond_t *cvp, const pthread_condattr_t *cattrp)

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

int pthread_cond_destroy (pthread_cond_t *cvp)

разрушает условную переменную, освобождая память.

int pthread_cond_wait (pthread_cond_t *cvp, const pthread_mutex_t *mp)

автоматически освобождает взаимоисключающую блокировку, указанную mp, а вызывающий поток блокируется по условной переменной, заданной cvp. Заблокированный поток разблокируется функциями pthread_cond_signal() и pthread_cond_broadcast(). Одной условной переменной могут быть заблокированы несколько потоков.

int pthread_cond_timedwait (pthread_cond_t *cvp, const pthread_mutex_t *mp, struct timespec *tp)

аналогична функции pthread_cond_wait(), но имеет третий аргумент, задающий интервал времени, после которого поток разблокируется (если этого не было сделано ранее).

int pthread_cond_signal (pthread_cond_t *cvp)

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

int pthread_cond_broadcast (pthread_cond_t *cvp)

разблокирует все потоки, ожидающие данную условную переменную.

Семафоры

Семафор представляет собой целочисленную переменную. Потоки могут наращивать (post) и уменьшать (wait) ее значение на единицу. Если поток пытается уменьшить семафор так, что его значение становится отрицательным, то поток блокируется. Поток будет разблокирован, когда какой-либо другой поток не увеличит значение семафора так, что он станет неотрицательным после уменьшения его первым (заблокированным) потоком.

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

В POSIX-версии средств многопотокового программирования используются те же самые семафоры, что и для межпроцессного взаимодействия.


#include <semaphore.h>
int sem_init (sem_t *sp, int pshared, unsigned int value)

инициализирует семафор, указанный аргументом sp, значением value. Если pshared=0, то область действия семафора - только один процесс, иначе - несколько процессов.

int sem_destroy (sem_t *sp)

разрушает семафор.

int sem_post (sem_t *sp)

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

int sem_wait (sem_t *sp)

пытается уменьшить значение семафора на 1. Если при этом значение семафора должно стать отрицательным, то поток блокируется.

int sem_trywait (sem_t *sp)

неблокирующая версия функции sem_wait().

Барьеры

Барьер используется для синхронизации работы нескольких потоков управления. Барьер характеризуется натуральным числом count, задающим количество синхронизируемых потоков. Поток управления, "подошедший" к барьеру (обратившийся к функции pthread_barrier), блокируется до момента накопления перед этим барьером указанного количества потоков count.

int pthread_barrier_init(pthread_barrier_t *bp, pthread_barrierattr_t *attr, unsigned count)

инициализирует барьер, выделяя необходимую память, устанавливая значения его атрибутов и назначая count "шириной" барьера. В настоящее время атрибуты барьеров не определены поэтому в качестве второго параметра функции pthread_barrier_init следует использовать NULL.

int pthread_barrier_destroy(pthread_barrier_t *bp)

разрушает барьер, освобождая выделенную память.

int pthread_barrier_wait(pthread_barrier_t *bp)

приостанавливает вызвавший данную функцию поток до момента накопления перед барьером count потоков. Заблокированный поток может быть прерван сигналом, при этом обработчик сигнала (если он был назначен) будет вызван на выполнение обычным образом. Выход из обработчика вернет поток в состояние ожидания, если к этому моменту требуемое количество count потоков еще не скопилось перед барьером.

Источники

  1. Многопоточность // Википедия. [2018—2018]. Дата обновления: 24.01.2018. URL: https://ru.wikipedia.org/?oldid=90481685 (дата обращения: 06.06.2018).
  2. Учебно-методические материалы к курсу"Разработка программных систем" [Электронный ресурс].URL: http://fedoruk.comcor.ru/Dev_bach/index.html (дата обращения: 20.05.2018)

Ссылки