Средства синхронизации потоков
Изучая взаимодействие между процессами, мы много внимания уделили средствам синхронизации процессов. У потоков тоже есть свои специальные средства синхронизации. Вернемся к первому примеру из предыдущей статьи, - программе threads. Напомню, что в том примере мы создавали два потока, используя одну и ту же функцию потока. В процессе создания каждого потока этой функции передавалось целочисленное значение (номер потока). При этом для передачи значения каждому потоку использовалась своя переменная. В той статье я подробно объяснил, почему использование одной и той же переменной для передачи значения функциям разных потоков может привести к ошибке.
Коротко говоря, - проблема заключалась в том, что мы не могли знать, когда именно новый поток начнет выполняться. С помощью средств синхронизации потоков мы можем решить эту проблему и использовать одну переменную для передачи значений обоим потокам. Рассмотрим модифицированный вариант программы threads, программу threads2 (вы найдете ее в в файле threads2.c).
#include <stdlib.h> #include <stdio.h> #include <errno.h> #include <pthread.h> #include <semaphore.h> sem_t sem; void * thread_func(void *arg) { int i; int loc_id = * (int *) arg; sem_post(&sem); for (i = 0; i < 4; i++) { printf("Thread %i is running\n", loc_id); sleep(1); } } int main(int argc, char * argv[]) { int id, result; pthread_t thread1, thread2; id = 1; sem_init(&sem, 0, 0); result = pthread_create(&thread1, NULL, thread_func, &id); if (result != 0) { perror("Creating the first thread"); return EXIT_FAILURE; } sem_wait(&sem); id = 2; result = pthread_create(&thread2, NULL, thread_func, &id); if (result != 0) { perror("Creating the second thread"); return EXIT_FAILURE; } result = pthread_join(thread1, NULL); if (result != 0) { perror("Joining the first thread"); return EXIT_FAILURE; } result = pthread_join(thread2, NULL); if (result != 0) { perror("Joining the second thread"); return EXIT_FAILURE; } sem_destroy(&sem); printf("Done\n"); return EXIT_SUCCESS; }
В новом варианте программы мы используем одну переменную id для передачи значения обоим потокам. Если вы скомпилируете и запустите программу threads2, то увидите, что она работает корректно. Секрет нашего успеха заключается в использовании средств синхронизации. Для синхронизации потоков мы задействовали семафоры. Читатели этой серии статей уже знакомы с семафорами System V, предназначенными для синхронизации процессов. В данном случае мы применяем семафоры другого типа – семафоры POSIX, которые специально предназначены для работы с потоками. Все объявления функций и типов, относящиеся к этим семафорам, можно найти в файле /usr/include/nptl/semaphore.h. Семафоры POSIX создаются (инициализируются) с помощью функции sem_init(3). Первый параметр функции sem_init() – указатель на переменную типа sem_t, которая служит идентификатором семафора. Второй параметр - pshared - в настоящее время не используется, и мы оставим его равным нулю. В третьем параметре функции sem_init() передается значение, которым инициализируется семафор. Дальнейшая работа с семафором осуществляется с помощью функций sem_wait(3) и sem_post(3). Единственным аргументом функции sem_wait() служит указатель на идентификатор семафора. Функция sem_wait() приостанавливает выполнение вызвавшего ее потока до тех пор, пока значение семафора не станет большим нуля, после чего функция уменьшает значение семафора на единицу и возвращает управление. Функция sem_post() увеличивает значение семафора, идентификатор которого был передан ей в качестве параметра, на единицу. Присвоив семафору значение 0, наша программа создает первый поток и вызывает функцию sem_wait(). Эта функция приостановит выполнение функции main() до тех пор, пока функция потока не вызовет функцию sem_post(), а это случится только после того как функция потока обработает значение переменной id. Таким образом, мы можем быть уверены, что в момент создания второго потока первый поток уже закончит работу с переменной id, и мы сможем использовать эту переменную для передачи данных второму потоку. После завершения обоих потоков мы вызываем функцию sem_destroy(3) для удаления семафора и высвобождения его ресурсов.
Семафоры – не единственное средство синхронизации потоков. Для разграничения доступа к глобальным объектам потоки могут использовать мьютексы. Все функции и типы данных, имеющие отношение к мьютексам, определены в файле pthread.h. Мьютекс создается вызовом функции pthread_mutex_init(3). В качестве первого аргумента этой функции передается указатель на переменную pthread_mutex_t, которая играет роль идентификатора нового мьютекса. Вторым аргументом функции pthread_mutex_init() должен быть указатель на переменную типа pthread_mutexattr_t. Эта переменная позволяет установить дополнительные атрибуты мьютекса. Если нам нужен обычный мьютекс, мы можем передать во втором параметре значение NULL. Для того чтобы получить исключительный доступ к некоему глобальному ресурсу, поток вызывает функцию pthread_mutex_lock(3), (в этом случае говорят, что «поток захватывает мьютекс»). Единственным параметром функции pthread_mutex_lock() должен быть идентификатор мьютекса. Закончив работу с глобальным ресурсом, поток высвобождает мьютекс с помощью функции pthread_mutex_unlock(3), которой также передается идентификатор мьютекса. Если поток вызовет функцию pthread_mutex_lock() для мьютекса, уже захваченного другим потоком, эта функция не вернет управление до тех пор, пока другой поток не высвободит мьютекс с помощью вызова pthread_mutex_unlock() (после этого мьютекс, естественно, перейдет во владение нового потока). Удаление мьютекса выполняется с помощью функции pthread_mutex_destroy(3). Стоит отметить, что в отличие от многих других функций, приостанавливающих работу потока, вызов pthread_mutex_lock() не является точкой останова. Иначе говоря, поток, находящийся в режиме отложенного досрочного завершения, не может быть завершен в тот момент, когда он ожидает выхода из pthread_mutex_lock().