Семафор представляет разделяемую целочисленную переменную, которая применяется для ограничения количества потоков, которые имеют доступ к некоторому коду или ресурсам. Семафоры позволяют ограничить доступ к участку кода только определенному количестиву потоков. Также с помощью семафоров можно заставить один поток ждать, пока другой не выполнит определенное действие, таким образом упорядочивая действия потоков.
В языке Си основной функционал по работе с семафорами определен в заголовочном файле semaphore.h. Непросредственно сам семафор представлен типом sem_t. Для работы с семафорами имеется ряд функций. Рассмотрим основные из них:
int sem_init(sem_t *sem, int pshared, unsigned value)
: инициализирует семафор. Параметры функции:
*sem
- указатель на инициализуемый семафор
pshared
- указывает, будет ли семафор общим для потоков или процессов. Если равен 0, то семаформ - общий для потоков, если ненулевое значение - то семафор общий для процессов
value
- начальное значение семафора
В случае успешной инициализации функция возвращает 0, иначе возвращается -1.
int sem_destroy(sem_t *sem)
: удаляет семафор
int sem_wait(sem_t *sem)
: уменьшает (блокирует) семафор. Если значение семафора больше нуля, то выполняется уменьшение, и поток продолжает работу. Если значение семафора равно нулю, то поток блокируется до тех пор, пока не станет возможным выполнить
уменьшение.
int sem_post(sem_t *sem)
: увеличивает (разблокирует) семафор. Если значение семафора после этого становится больше нуля, то другой поток,
который ожидает получение семафора и заблокирован с помощью вызова sem_wait(), проснётся и заблокирует семафор.
sem_destroy(sem_t *sem)
: удудаляет семафораляет семафор
Рассмотрим применение семафоров на небольшом примере:
#include <semaphore.h> #include <pthread.h> #include <stdio.h> #include <unistd.h> sem_t sem; // семафор int result1 = 0; int result2 = 0; // Функция Потока 1 void* thread1_work(void* args) { result1 = 1; // условная работа потока - установка переменной puts("Thread1 works"); sem_post(&sem); // увеличиваем значение семафора return NULL; } // Функция Потока 2 void* thread2_work(void* args) { result2 = 2; // условная работа потока - установка переменной puts("Thread2 works"); sem_post(&sem); // увеличиваем значение семафора return NULL; } // Функция Потока 3 void* thread3_work(void* arg) { puts("Thread3 waits"); sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его printf("result1 = %d result2 = %d\n", result1, result2); return NULL; } int main(void) { sem_init(&sem, 0, 0); // инициализируем семафор pthread_t thread1, thread2, thread3; pthread_create(&thread3, NULL, thread3_work, NULL); sleep(2); // чтобы Поток 3 начал выполняться раньше pthread_create(&thread1, NULL, thread1_work, NULL); pthread_create(&thread2, NULL, thread2_work, NULL); sem_destroy(&sem); // удаляем семафор pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_join(thread3, NULL); return 0; }
Сначала запустим программу и посмотрим наи ее консольный вывод:
Thread3 waits Thread1 does work Thread2 does work result1 = 1 result2 = 2
Разберем эту программу. Здесь семафор применяется для установки очередности действий потоков. Вначале определяем сам семафор:
sem_t sem; // семафор
Для демонстрации также определяем две глобальных переменных, значения которых будут вычисляться в двух потоках и которые по умолчанию равны 0:
int result1 = 0; int result2 = 0;
В функции main инициализируем семафор числом 0:
sem_init(&sem, 0, 0);
Далее запускаем три потока. Причем поток thread3 запускается первым - он выполняет функцию thread3_work:
void* thread3_work(void* arg) { puts("Thread3 waits"); sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его printf("result1 = %d \tresult2 = %d\n", result1, result2); return NULL; }
Поток thread3 выполняет функцию sem_wait()
и тем самым проверяет значение семафора: если оно больше 0, то поток уменьшает значение семафора и продолжает работу. Но поскольку
при инициализации семафору назначено число 0, то данный поток начинает ждать, пока семафор не станет равен больше 0. Причем поскольку здесь 2 раза вызывается sem_wait()
, то где-то во внешнем коде надо два раза увеличить семафор,
чтобы поток thread3 перешел к выводу на консоль переменных result1 и result2.
После потока thread3 запускаются потоки thread1 и thread2, которые выполняют однотиные функции thread1_work и thread2_work соответственно. К примеру, возьмем поток thread1:
void* thread1_work(void* args) { result1 = 1; // условная работа потока - установка переменной puts("Thread1 works"); sem_post(&sem); // увеличиваем значение семафора return NULL; }
Поток thread1 выполняет некоторую работу - устанавливает переменную result1 и затем с помощью функции sem_post()
увеличивает значение семафора. Аналогично действует поток
thread2, который устанавливает переменную result2 и увеличивает значение семафора.
И только после того, как потоки thread1 и thread2 увеличат по разу семафор, поток thread3 сможет уменьшить это значение до 0 и переходит выводу значения переменных result1 и result2 на консоль. В итоге сначала выполняться оба потока thread1 и thread2 и только потом thread3.
После завершения работы удаляем семафор:
sem_destroy(&sem);
Рассмотрим еще один пример. Например, у нас есть повар, который готовит еду, и есть человек, который потребляет приготовленную еду. И печальная реальность состоит в том, что, чтобы поесть еду, ее надо сначала сготовить. Пусть у нас будет следующая программа:
#include <semaphore.h> #include <pthread.h> #include <stdio.h> #include <unistd.h> sem_t sem; // семафор // Функция Повара void* cook(void* args) { puts("Breakfast is ready"); // повар готовит завтрак sem_post(&sem); // увеличиваем значение семафора sleep(2); puts("Lunch is ready"); // повар готовит обед sem_post(&sem); // увеличиваем значение семафора sleep(2); puts("Dinner is ready"); // повар готовит ужин sem_post(&sem); // увеличиваем значение семафора return NULL; } // Функция уничтожителя еды void* eat(void* arg) { sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его puts("Having breakfast"); // едим завтрак sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его puts("Having lunch"); // едим обед sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его puts("Having dinner"); // едим ужин return NULL; } int main(void) { sem_init(&sem, 0, 0); // инициализируем семафор pthread_t chief, eater; pthread_create(&eater, NULL, eat, NULL); sleep(2); // аппетит приходит вовремя, а еду опять задерживают на 2 секунды pthread_create(&chief, NULL, cook, NULL); sem_destroy(&sem); // удаляем семафор pthread_join(chief, NULL); pthread_join(eater, NULL); return 0; }
За повара здесь отвечает поток chief, который запускает функцию cook. В этой функции повар последовательно готовит завтрак, обед и ужин, при этом увеличивая значение семафора:
puts("Breakfast is ready"); // повар готовит завтрак sem_post(&sem); // увеличиваем значение семафора sleep(2); puts("Lunch is ready"); // повар готовит обед sem_post(&sem); // увеличиваем значение семафора sleep(2); puts("Dinner is ready"); // повар готовит ужин sem_post(&sem); // увеличиваем значение семафора
За потребителя еды отвечает поток eater, который запускает функцию eat. В этой функции с помощью функции sem_wait ожидаем, когда семафор станет больше 0 (когда повар увеличит значение семафора) и уменьшаем его (условно потребляем пищу):
sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его puts("Having breakfast"); // едим завтрак sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его puts("Having lunch"); // едим обед sem_wait(&sem); // ожидаем, когда семафор станет больше 0 и уменьшаем его puts("Having dinner"); // едим ужин
Консольный вывод программы:
Breakfast is ready Having breakfast Lunch is ready Having lunch Dinner is ready Having dinner
Распространенная функция семафоров - ограничение количества потоков, которые имеют доступ к некоторому участку кода. Например, в обоих примерах выше по умолчанию семафор не предусматривал ограничения потоков. Теперь посмотрим, как это сделать Например, у нас такая задача: есть некоторое число читателей, которые приходят в библиотеку два раза в день и что-то там читают. И пусть у нас будет ограничение, что единовременно в библиотеке не может находиться больше трех читателей. Данную задачу очень легко решить с помощью семафоров:
#include <semaphore.h> #include <pthread.h> #include <stdio.h> #include <unistd.h> sem_t sem; // семафор void* read_books(void* arg) { char* reader = (char*) arg; // идентификатор читателя int count = 2;// счетчик чтения while (count > 0) { sem_wait(&sem); // ожидаем, когда освободиться место printf("%s enters the library \n", reader); printf("%s reads \n", reader); sleep(1); printf("%s leaves the library \n", reader); sem_post(&sem); // освобождаем место count--; sleep(1); } return NULL; } int main(void) { sem_init(&sem, 0, 3); // инициализируем семафор pthread_t readers[5]; pthread_create(&readers[0], NULL, read_books, "Reader 1"); pthread_create(&readers[1], NULL, read_books, "Reader 2"); pthread_create(&readers[2], NULL, read_books, "Reader 3"); pthread_create(&readers[3], NULL, read_books, "Reader 4"); pthread_create(&readers[4], NULL, read_books, "Reader 5"); sem_destroy(&sem); // удаляем семафор pthread_exit(NULL); // ждем завершения всех потоков и завершаем текущий поток return 0; }
При инициализации семафора устанавливаем для него начальное значение - 3:
sem_init(&sem, 0, 3);
То есть одновременно доступ к некоторому коду могут иметь не более 3 потоков (иначе говоря в библиотеке одновременно могут находиться только 3 читателя)
Массив readers из 5 потоков представляет 5 читателей (больше чем ограничение семафора - число 3). При запуске каждого потока начинает выполняться функция read_books, в которую передается строковой идентификатор читателя в виде "ReaderN".
В начале функции read_books для ожидания получения семафора используется функция sem_wait()
:
void* read_books(void* arg) { char* reader = (char*) arg; // идентификатор читателя int count = 2;// счетчик чтения while (count > 0) { sem_wait(&sem); // ожидаем, когда освободиться место
После того, как в семафоре освободится место (семафор станет больше 0), данный поток вычитает из семафора 1 - читатель заполняет свободное место и начинает выполнять все дальнейшие действия.
printf("%s enters the library \n", reader); printf("%s reads \n", reader); sleep(1); printf("%s leaves the library \n", reader);
После окончания чтения мы высвобождаем семафор с помощью функции sem_post
(увеличиваем значение семафора - читатель покидает библиотеку):
sem_post(&sem); // освобождаем место
После этого в семафоре освобождается одно место, которое заполняет другой поток. Пример консольного вывода:
Reader 1 enters the library Reader 1 reads Reader 3 enters the library Reader 3 reads Reader 2 enters the library Reader 2 reads Reader 1 leaves the library Reader 3 leaves the library Reader 2 leaves the library Reader 4 enters the library Reader 4 reads Reader 5 enters the library Reader 5 reads Reader 1 enters the library Reader 1 reads Reader 4 leaves the library Reader 5 leaves the library Reader 2 enters the library Reader 2 reads Reader 3 enters the library Reader 3 reads Reader 1 leaves the library Reader 4 enters the library Reader 4 reads Reader 2 leaves the library Reader 3 leaves the library Reader 5 enters the library Reader 5 reads Reader 4 leaves the library Reader 5 leaves the library