Семафоры

Последнее обновление: 06.02.2024

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

В языке Си основной функционал по работе с семафорами определен в заголовочном файле 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 
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850