Семафоры: введение
Семафоры
Семафор – это объект, который используется для контроля доступа нескольких потоков до общего ресурса. В общем случае это какая-то переменная, состояние которой изменяется каждым из потоков. Текущее состояние переменной определяет доступ к ресурсам.
В pthreads семафор – это переменная типа sem_t, которая может находиться в заданном числе состояний. Каждый поток может увеличить счётчик семафора, или уменьшить его. В литературе операция увеличения значения счётчика называется V (от датского verhogen – увеличивать, или vrijgave - освобождать). Для простоты можно запоминать как английское vacate (освобождать). Уменьшение счётчика – это операция P (proberen – тестировать, или passeren – проходить, или pakken – захватывать, или даже probeer te verlagen – попытаться уменьшить), или по-английски procure – добывать.
Замечание: иногда мьютекс называют двоичным семафором, указывая не то, что мьютекс может находиться в двух состояниях. Но здесь есть важное отличие: разблокировать мьютекс может только тот поток, который его заблокировал, семафор же может «разблокировать» любой поток.
Семафоры описаны в библиотеке semaphore.h. Работа с семаформаи похожа на работу с мьютексами. Сначала необходимо инициализировать семафор с помощью функции
int sem_init(sem_t *sem, int pshared, unsigned int value);
где sem – это указатель на семафор, pshared – флаг,указывающий, должен ли семафор быть расшарен при использовании функции fork(), value – начальное значение семафора.
Далее, для ожидания доступа используется функция
int sem_wait(sem_t *sem);
Если значение семафора отрицательное, то вызывающий поток блокируется до тех пор, пока один из потоков не вызовет sem_post
int sem_post(sem_t *sem);
Эта функция увеличивает значение семафора и разблокирует ожидающие потоки. Как обычно, в конце необходимо уничтожить семафор
int sem_destroy(sem_t *sem);
Кроме тго, можно получить текущее значение семаформа
int sem_getvalue(sem_t *sem, int *valp);
Здесь в переменную valp будет помещено значение счётчика.
Обратимся опять к старому примеру. Два потока изменяют одну переменную, копируя её в локальную переменную. Один поток увеличивает значение, а второй уменьшает 100 раз. Таким образом, в конце должен быть 0. Из-за совместного доступа к переменной получаем каждый раз разное значение
#define _CRT_SECURE_NO_WARNINGS
#include <pthread.h>
#include <stdio.h>
#include <conio.h>
#include <Windows.h>
static int counter = 0;
void* worker1(void* args) {
int i;
int local;
for (i = 0; i < 100; i++) {
local = counter;
printf("worker1 - %d\n", local);
local++;
counter = local;
Sleep(10);
}
}
void* worker2(void* args) {
int i;
int local;
for (i = 0; i < 100; i++) {
local = counter;
printf("worker 2 - %d\n", local);
local--;
counter = local;
Sleep(10);
}
}
void main() {
pthread_t thread1;
pthread_t thread2;
pthread_create(&thread1, NULL, worker1, NULL);
pthread_create(&thread2, NULL, worker2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("== %d", counter);
_getch();
}
Синхронизируем теперь их с помощью семафора. Рассмотрим сначала пример, когда счётчик равен 1, значит, после того, как один из потоков уменьшит значение семафора, то второй вынужден будет ждать доступа к ресурсу (в данном случае, это тело функции worker1 или worker2)
#define _CRT_SECURE_NO_WARNINGS
#include <pthread.h>
#include <stdio.h>
#include <conio.h>
#include <Windows.h>
#include <semaphore.h>
sem_t semaphore;
static int counter = 0;
void* worker1(void* args) {
int i;
int local;
for (i = 0; i < 100; i++) {
sem_wait(&semaphore);
local = counter;
printf("worker1 - %d\n", local);
local++;
counter = local;
Sleep(10);
sem_post(&semaphore);
}
}
void* worker2(void* args) {
int i;
int local;
for (i = 0; i < 100; i++) {
sem_wait(&semaphore);
local = counter;
printf("worker 2 - %d\n", local);
local--;
counter = local;
Sleep(10);
sem_post(&semaphore);
}
}
void main() {
pthread_t thread1;
pthread_t thread2;
sem_init(&semaphore, 0, 1);
pthread_create(&thread1, NULL, worker1, NULL);
pthread_create(&thread2, NULL, worker2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
sem_destroy(&semaphore);
printf("== %d", counter);
_getch();
}
Здесь всё совершено понятно. Увеличим теперь значение счётчика до 3, например. Заменим
sem_init(&semaphore, 0, 1);
на
sem_init(&semaphore, 0, 3);
Теперь мы получаем ту же неразбериху. В конце counter вряд ли будет равен нулю. Это случается от того, что после уменьшения значения семафора он ещё не равен нулю (3 - 1 = 2), из-за чего блокировки не происходит. Так как нет блокировки, ещё два потока могут изменить перменную.
Мьютексы и семафоры
Рассмотрим аналогию: мьютекс можно сравнить с ключом от туалета на заправке. Если у вас есть ключ, то вы можете зайти в туалет, и туда больше никто не зайдёт. Если у вас нет ключа, то надо ждать, и в туалет вы зайти не можете. Мьютекс ограничивает доступ к общему ресурсу и позволяет в один момент времени пользоваться этим ресурсом только одному потоку.
Этот подход, к сожалению, не масштабируется на два туалета. И семафор не может разрешить проблему: по аналогии, мы имели бы на заправке два одинаковых ключа для двух одинаковых туалетов. Если один человек уже зашёл в туалет, то у второго оказался бы дубликат, способный открыть дверь туалета (свободного или занятого). Для предупреждения такой ситуации необходимо вводить флаг «туалет занят» (а это два мьютекса…), или же с самого начала использовать два разных ключа (т.е., два мьютекса). Семафор не помогает нам решать проблему доступа до набора идентичных ресурсов.
Цель использования семафора – оповещение одним потоком другого. Когда мы используем мьютекс, сначала мы его захватываем, потом используем ресурс, потом отдаём, и поступаем так всегда, потому что это обеспечивает защиту ресурса. В противоположность этому, потоки, которые используют семафор, либо ждут, либо сигналят, но не делают того и другого вместе. Например, один поток «нажимает» на кнопку, а второй отвечает на нажатие выводом сообщения.
Мьютекс:
//Поток 1
захватить_мьютекс(ключи_от_уборной)
использовать_ресурс
отдать_мьютекс(ключи_от_уборной)
//Поток 2
захватить_мьютекс(ключи_от_уборной)
использовать_ресурс
отдать_мьютекс(ключи_от_уборной)
Семафор
//Поток 1
послать_сигнал(состояние_кнопки)
//Поток2
принять_сигнал(состояние_кнопки)
Важно также, что сигнал может быть послан из обработчика прерываний, и не является блокирующей операцией, поэтому часто используется в ОС реального времени.
Рассмотрим следующий пример. Есть три потока. Все три создаются одновременно, но второй должен работать только после того, как отработает первый, а третий после того, как отработает второй.
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_RAND_S
#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>
#ifdef _WIN32
#include <conio.h>
#include <Windows.h>
#define Sleep(x) Sleep(x)
#define wait() _getch()
#else
#include <unistd.h>
#define Sleep(x) sleep(x)
#define wait() scanf("1")
#endif
sem_t semaphore1;
sem_t semaphore2;
static int counter = 0;
void* waiter(void* args) {
Sleep(3000);
sem_wait(&semaphore2);
printf("waiter work!");
}
void* signaler1(void* args) {
Sleep(1500);
sem_post(&semaphore1);
printf("signaler1 work!");
}
void* signaler2(void* args) {
Sleep(2500);
sem_wait(&semaphore1);
sem_post(&semaphore2);
printf("signaler2 work!");
}
void main() {
pthread_t thread1;
pthread_t thread2;
pthread_t thread3;
sem_init(&semaphore1, 0, 0);
sem_init(&semaphore2, 0, 0);
pthread_create(&thread1, NULL, waiter, NULL);
pthread_create(&thread2, NULL, signaler1, NULL);
pthread_create(&thread3, NULL, signaler2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
sem_destroy(&semaphore1);
sem_destroy(&semaphore2);
wait();
}
