У потоков есть свои специальные средства синхронизации. Вернемся к первому примеру. В том примере мы создавали два потока, используя одну и ту же функцию потока. В процессе создания каждого потока этой функции передавалось целочисленное значение (номер потока). При этом для передачи значения каждому потоку использовалась своя переменная. Использование одной и той же переменной для передачи значения функциям разных потоков может привести к ошибке.
Проблема заключалась в том, что мы не могли знать, когда именно новый поток начнет выполняться. С помощью средств синхронизации потоков мы можем решить эту проблему и использовать одну переменную для передачи значений обоим потокам. Рассмотрим модифицированный вариант программы, с использованием таких средств, как семафоров. Семафоры - счетчики, которые разрешают синхронизировать множественные потоки.
//gcc semaphore.c -D_REENTERANT -I/usr/include/nptl -o semaphore -L/usr/lib/nptl -lpthread (Используйте эту строчку для компиляции в ОС Linux)
//gcc semaphore.c -D_REENTERANT -I/usr/include/nptl -o semaphore -L/usr/lib/nptl -lrt -lthread (Используйте эту строчку для компиляции в ОС Solaris)
#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 id = * (int *) arg;
sem_post(&sem);//инкремент значения семафора
for (i = 0; i < 4; i++) {
printf("Hello from thread %d\n", id);
sleep(1);
}
}
int main(int argc, char * argv[])
{
int id=1;
int result;
pthread_t thread1, thread2;
sem_init(&sem, 0, 0);//инициализация семафора, со значением 0
result = pthread_create(&thread1, NULL, thread_func, &id);
if (result != 0)
{
perror("While creating thread");
return 1;
}
sem_wait(&sem);//приостановление выполнения функции main() до тех пор, пока функция потока не вызовет функцию sem_post()
id = 2;
result = pthread_create(&thread2, NULL, thread_func, &id);
if (result != 0)
{
perror("While creating thread");
return 1;
}
result = pthread_join(thread1, NULL);
if (result != 0)
{
perror("While joining thread");
return 2;
}
result = pthread_join(thread2, NULL);
if (result != 0)
{
perror("While joining thread");
return 2;
}
sem_destroy(&sem); //удаление семафора, освобождение его ресурсов
printf("Done\n");
return 0;
}
В новом варианте программы мы используем одну переменную id для передачи значения обоим потокам. Для синхронизации потоков мы задействовали семафоры. Семафоры инициализируются с помощью функции 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) для удаления семафора и высвобождения его ресурсов.
Семафоры ? не единственное средство синхронизации потоков. Для разграничения доступа к глобальным объектам потоки могут использовать мьютексы.
Мью?текс (англ. mutex, от mutual exclusion ? взаимное исключение) ? одноместный семафор, служащий в программировании для синхронизации одновременно выполняющихся потоков.
Это один из вариантов семафорных механизмов для организации взаимного исключения. Они реализованы во многих ОС, их основное назначение ? организация взаимного исключения для потоков из одного и того же или из разных процессов.
Мьютексы ? это простейшие двоичные семафоры, которые могут находиться в одном из двух состояний ? отмеченном или неотмеченном (открыт и закрыт соответственно). Когда какой-либо поток, принадлежащий любому процессу, становится владельцем объекта mutex, последний переводится в неотмеченное состояние. Если задача освобождает мьютекс, его состояние становится отмеченным.
Организация последовательного доступа к ресурсам с использованием мьютексов становится несложной, поскольку в каждый конкретный момент только один поток может владеть этим объектом. Для того, чтобы мьютекс стал доступен потокам, принадлежащим разным процессам, при создании ему необходимо присвоить имя. Потом это имя нужно передать ?по наследству? задачам, которые должны его использовать для взаимодействия. Для этого вводятся специальные системные вызовы (CreateMutex), в которых указывается начальное значение мьютекса и его имя.
Для работы с мьютексом имеется несколько функций. Помимо уже упомянутой функции создания такого объекта (CreateMutex), есть функции открытия (OpenMutex) и функция освобождения этого объекта (ReleaseMutex). Конкретные обращения к этим функциям и перечни передаваемых и получаемых параметров нужно смотреть в документации на соответствующую ОС.
Единственная задача мьютекса ? защита объекта от доступа к нему других потоков, отличных от того, который завладел мьютексом. Если другому потоку будет нужен доступ к переменной, защищённой мьютексом, то этот поток просто засыпает до тех пор, пока мьютекс не будет освобождён.
Все функции и типы данных, имеющие отношение к мьютексам, определены в файле 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().