Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Параллельное программирование над общей памятью. POSIX Threads. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
1 Мб
Скачать

2.Blaise Barney. POSIX Threads Programming [Electronic resource] // Lawrence Livermore National Laboratory. – URL: https://computing.llnl.gov/tutorials/pthreads/ (accessed: 28.07.2017).

3.Многопоточное программирование с использованием POSIX Threads [Электронный ресурс] // Национальный открытый университет «ИНТУИТ». – URL: http://www.intuit.ru/studies/professional_skill_improvements/16994/info (дата обращения: 04.08.2017).

4.Pthreads: Потоки в русле POSIX [Электронный ресурс] // Хабрахабр. – URL: https://habrahabr.ru/post/326138/ (дата обращения: 08.08.2017).

21

2. СИНХРОНИЗАЦИЯ ПОТОКОВ

МЬЮТЕКСЫ

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

Pthreads предоставляет различные средства синхронизации потоков. К наиболее часто используемым относятся мьютексы, барьеры, спинлоки и условные переменные.

Мьютекс (англ. mutex, от mutual exclusion – взаимное исключение) – объект синхронизации, обеспечивающий взаимно исключающий доступ к ресурсу. Только поток, владеющий мьютексом, может работать с ресурсом.

В Pthreads мьютекс представляется специальным типом данных: pthread_mutex_t. Для использования мьютекса необходимо создать переменную типа pthread_mutex_t и инициализировать ее с помощью функции pthread_mutex_init(). Потоки, желающие получить доступ к ресурсу, защищенному мьютексом, должны его захватить. В любой момент только один поток может владеть мьютексом. Если несколько потоков пытаются одновременно захватить один и тот же мьютекс, только один поток сможет это сделать. Захват мьютекса выполняется с помощью функции pthread_mutex_lock() либо функции pthread_mutex_trylock(). Разница между этим функциями состоит в том, что вызов pthread_mutex_lock() приводит к блокированию выполнения потока, если мьютекс уже захвачен другим пото-

22

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

Тип объекта синхронизации:

pthread_mutex_t

Прототипы функций:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex);

int pthread_mutex_destroy(pthread_mutex_t *mutex);

Параметры функций:

mutex – указатель на переменную типа «мьютекс»;

attr – указатель на атрибуты, которые получит мьютекс при инициализации (см. ниже); если указано NULL, то мьютекс получит атрибуты по умолчанию.

Сообщения об ошибках:

ноль – ошибки отсутствуют; pthread_mutex_init():

EAGAIN – недостаточно ресурсов (кроме памяти) для инициализации мьютекса;

ENOMEM – недостаточно памяти для инициализации мьютекса;

EPERM – недостаточно прав для выполнения операции;

pthread_mutex_lock() и pthread_mutex_trylock():

EAGAIN – мьютекс не может быть захвачен из-за превышения максимального количества рекурсивных захватов;

23

ENOTRECOVERABLE – невозможно восстановить целостное состояние данных, защищенных мьютексом;

EOWNERDEAD – поток, владевший мьютексом, завершился, не освободив его;

pthread_mutex_lock():

EDEADLK – текущий поток уже захватил мьютекс; pthread_mutex_trylock():

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

pthread_mutex_unlock():

EPERM – поток не владеет мьютексом.

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

Рассмотрим пример модельной задачи: имеется очередь заданий и два потока-исполнителя. Каждый поток забирает из очереди текущее задание, сдвигает указатель текущего задания на следующее задание, после чего выполняет взятое задание. Процесс повторяется до тех пор, пока все задания в очереди не будут выполнены. В примере 4 приводится программа, решающая эту задачу с использованием мьютекса.

Для обработки ошибок создан специальный макрос err_exit, который позволяет сократить объем кода, осуществляющего проверки на ошибки.

Пример 4. Использование мьютексов

#include <cstdlib> #include <iostream> #include <cstring> #include <pthread.h>

using namespace std;

#define err_exit(code, str) { cerr << str << ": " << strerror(code) \ << endl; exit(EXIT_FAILURE); }

const int TASKS_COUNT = 10;

int task_list[TASKS_COUNT]; // Массив заданий

24

int current_task = 0;

//

Указатель на текущее задание

pthread_mutex_t mutex;

//

Мьютекс

void do_task(int task_no)

{

/* Сюда необходимо поместить код, выполняющий задание */

}

void *thread_job(void *arg)

{

int task_no; int err;

// Перебираем в цикле доступные задания while(true) {

//Захватываем мьютекс для исключительного доступа

//к указателю текущего задания (переменная

//current_task)

err = pthread_mutex_lock(&mutex); if(err != 0)

err_exit(err, "Cannot lock mutex");

//Запоминаем номер текущего задания, которое будем исполнять task_no = current_task;

//Сдвигаем указатель текущего задания на следующее current_task++;

//Освобождаем мьютекс

err = pthread_mutex_unlock(&mutex); if(err != 0)

err_exit(err, "Cannot unlock mutex");

//Если запомненный номер задания не превышает

//количества заданий, вызываем функцию, которая

//выполнит задание.

//В противном случае завершаем работу потока if(task_no < TASKS_COUNT)

do_task(task_no);

else

return NULL;

}

}

25

int main()

 

{

 

pthread_t thread1, thread2;

// Идентификаторы потоков

int err;

// Код ошибки

//Инициализируем массив заданий случайными числами for(int i=0; i<TASKS_COUNT; ++i)

task_list[i] = rand() % TASKS_COUNT;

//Инициализируем мьютекс

err = pthread_mutex_init(&mutex, NULL); if(err != 0)

err_exit(err, "Cannot initialize mutex"); // Создаём потоки

err = pthread_create(&thread1, NULL, thread_job, NULL); if(err != 0)

err_exit(err, "Cannot create thread 1");

err = pthread_create(&thread2, NULL, thread_job, NULL); if(err != 0)

err_exit(err, "Cannot create thread 2"); pthread_join(thread1, NULL); pthread_join(thread2, NULL);

// Освобождаем ресурсы, связанные с мьютексом pthread_mutex_destroy(&mutex);

}

Работа с атрибутами мьютекса производится по аналогии с работой с атрибутами потока. Атрибуты мьютекса представляются типом pthread_mutexattr_t. Переменную этого типа перед использованием необходимо инициализировать с помощью функции pthread_ mutexattr_init(), а после использования освободить связанные с ней ресурсы с помощью функции pthread_mutexattr_destroy().

Конкретный набор атрибутов мьютекса зависит от реализации. Чаще всего используются атрибут type (тип мьютекса), который мож-

но установить функцией pthread_mutexattr_settype(), а прочитать функцией pthread_mutexattr_gettype(), и атрибут pshared –

для работы с ним применяются функции pthread_mutexattr_ setpshared() и pthread_mutexattr_getpshared().

26

Прототипы функций:

int pthread_mutexattr_init(pthread_mutexattr_t *attr); int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);

int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);

int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);

Параметры функций:

attr – атрибуты мьютекса;

type – тип мьютекса (табл. 3);

pshared – видимость мьютекса (табл. 4);

Сообщения об ошибках:

ноль – ошибки отсутствуют; pthread_mutexattr_init():

ENOMEM – недостаточно памяти для инициализации объекта с атрибутами мьютекса;

pthread_mutexattr_settype():

EINVAL – значение атрибута «тип мьютекса» некорректно;

pthread_mutexattr_setpshared():

EINVAL – значение атрибута «видимость мьютекса» выходит за границы допустимых значений.

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

PTHREAD_MUTEX_NORMAL не допускает повторного захвата мьютекса; при попытке повторного захвата мьютекса потоком, который является его владельцем, происходит дедлок;

27

PTHREAD_MUTEX_ERRORCHECK не допускает повторного захвата мьютекса; предотвращает возникновение дедлоков, возвращая ошибку при попытке повторного захвата неосвобожденного мьютекса его владельцем;

PTHREAD_MUTEX_RECURSIVE допускает рекурсивный захват мьютекса одним и тем же потоком, однако затем поток должен столько же раз освободить мьютекс;

PTHREAD_MUTEX_DEFAULT является значением по умолчанию. Реа-

лизация Pthreads может отображать это значение на один из других трех типов.

Сводные характеристики различных типов мьютексов приведены в табл. 3.

 

 

Таблица 3

Возможные значения атрибута type

 

 

 

Значение

Повторная

Освобождение

чужого

блокировка

 

илисвободного

 

 

PTHREAD_MUTEX_NORMAL

Дедлок

Неопределено

PTHREAD_MUTEX_ERRORCHECK

Возвращаетошибку

Возвращаетошибку

PTHREAD_MUTEX_RECURSIVE

Допускается

Возвращаетошибку

PTHREAD_MUTEX_DEFAULT

Неопределено

Неопределено

Атрибут мьютекса pshared определяет возможность использова-

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

Таблица 4

Возможные значения атрибута pshared

Значение

Описание

PTHREAD_PROCESS_PRIVATE

Позволяет использовать мьютекс в рамках

 

одного процесса (синхронизироваться мо-

 

гут только потоки, принадлежащие одному

 

процессу). Является значением по умолча-

 

нию

PTHREAD_PROCESS_SHARED

Позволяет использовать мьютекс для син-

 

хронизации потоков, принадлежащих раз-

 

ным процессам (при этом процессы долж-

 

ны иметь доступ к памяти, где инициали-

 

зирован мьютекс)

28

СПИНЛОКИ

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

Вслучае мьютекса, если потоку не удается захватить блокировку, происходит переключение в режим ядра ОС3, где поток переводится в состояние «спящий» (см. раздел «Планирование потоков») и перестает расходовать ресурсы системы до момента освобождения мьютекса.

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

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

Тип объекта синхронизации:

pthread_spinlock_t

Прототипы функций:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_lock(pthread_spinlock_t *lock); int pthread_spin_trylock(pthread_spinlock_t *lock); int pthread_spin_unlock(pthread_spinlock_t *lock);

3 В зависимости от операционной системы и конкретной реализации стандарта POSIX Threads технические детали могут существенно различаться.

29

Параметры функций:

lock – указатель на переменную типа «спинлок»;

pshared – видимость спинлока; возможные значения приведены в табл. 3.

Сообщения об ошибках:

ноль – ошибки отсутствуют; pthread_spin_init():

EAGAIN – недостаточно ресурсов (кроме памяти) для инициализации спинлока;

ENOMEM – недостаточно памяти для инициализации спинлока;

pthread_spin_trylock():

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

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

 

Пример 5. Использование спинлоков

...

 

pthread_spinlock_t lock;

// Спинлок

...

void *thread_job(void *arg)

{

...

// Захватываем спинлок

err = pthread_spin_lock(&lock); if(err != 0)

err_exit(err, "Cannot lock spinlock");

// Запоминаем номер текущего задания, которое будем исполнять task_no = current_task;

30

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]