Семафоры: введение

Теги: pthreads, sem_t, sem_init, sem_wait, sem_destroy, sem_post, sem_getvalue



Семафоры

Семафор – это объект, который используется для контроля доступа нескольких потоков до общего ресурса. В общем случае это какая-то переменная, состояние которой изменяется каждым из потоков. Текущее состояние переменной определяет доступ к ресурсам.

В 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();
}
Q&A

Всё ещё не понятно? – пиши вопросы на ящик email
Мьютексы: введение