Semaphore е примитив за синхронизация, който управлява достъпа до споделен ресурс чрез брояч и опашка от чакащи нишки. Според Wikipedia, 2024, семафорът е предложен от Едсгер Дайкстра през 1965 г. за решаване на проблеми с многонишковото взаимодействие. Инструментът позволява ограничаване на броя нишки, които едновременно работят с критична секция.
Основни точки
Semaphore — е примитив за синхронизация, който използва брояч за управление на достъпа до споделен ресурс. Концепцията е предложена от Едсгер Дайкстра през 1965 г. и става основа на всички съвременни механизми за синхронизация в операционните системи.
Семафорът представлява целочислена променлива с две атомарни операции: wait (acquire) и signal (release). Операцията wait намалява брояча, а signal го увеличава. Когато броячът достигне нула, нишката, която извиква wait, се блокира до изпълнението на signal от друга нишка.
Основното предназначение на семафора е защита на критичните секции от едновременен достъп на множество нишки. За разлика от mutex, семафорът не изисква обвързване с нишката-собственик, което го прави подходящ за по-широк кръг от координационни задачи.
Концепцията за семафора възниква в контекста на операционната система THE, разработена в Technische Hogeschool Eindhoven. Дайкстра формализира семафора като математическа абстракция, доказвайки неговата достатъчност за реализация на всякакви примитиви за синхронизация.
Механизмът на семафора се основава на две атомарни операции и вътрешна опашка за изчакване. При извикване на acquire нишката проверява стойността на брояча и или продължава изпълнението, или се блокира до освобождаването на ресурса.
При създаване на семафора се задава начална стойност на брояча на разрешения. Всяко извикване на acquire намалява брояча с 1. Ако след това броячът стане отрицателен, нишката се блокира. Операцията release увеличава брояча и събужда една от чакащите нишки.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} работи")
} finally {
semaphore.release()
}
}
Когато нишка извика acquire с нулев брояч, ОС я поставя в опашката FIFO на семафора. Нишката преминава в състояние BLOCKED, без да консумира процесорно време. След извикване на release, първата нишка в опашката преминава в състояние RUNNABLE и получава достъп до ресурса.
В теорията на синхронизацията се различават два основни типа семафори: двоичен (binary) и броячeн (counting). Изборът на тип зависи от конкретната задача за управление на достъпа до ресурси.
Двоичният семафор приема само стойности 0 и 1. По поведение наподобява mutex, но без изискване за собственост — всяка нишка може да изпълни release. Такива семафори са удобни за реализация на флагове за готовност и събития между нишки.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Данните са готови")
}
Броячeн семафор може да приема всяка неотрицателна стойност. Използва се за управление на пул от еднородни ресурси, където са налични няколко инстанции. Например пул от 5 мрежови връзки: всяко acquire заема една връзка, release я връща в пула.
Броячните семафори са незаменими при ограничаване на скоростта на достъп до външни услуги и реализация на пулове от нишки. Те позволяват прецизен контрол на степента на паралелизъм без ръчно управление на нишки.
| Параметър | Двоичен семафор | Броячeн семафор |
|---|---|---|
| Диапазон | 0 или 1 | от 0 до N |
| Нишки едновременно | 1 | до N |
| Приложение | сигнализация, флагове | пулове ресурси, rate limiting |
Разработчиците често бъркат семафора и mutex, въпреки че между тях съществуват фундаментални разлики. Разбирането на тези разлики е от решаващо значение за избора на правилния механизъм за синхронизация в проекта.
Ключовата разлика — концепцията за собственост. Mutex винаги знае коя нишка го е завладяла и само тя може да го освободи. Семафорът няма собственик: всяка нишка може да извика release, дори без да извиква acquire. Това прави mutex по-безопасен за защита на данни, а семафора — по-гъвкав за координация.
На практика mutex е по-бърз за просто взаимно блокиране благодарение на оптимизациите за типичния сценарий. Семафорът изисква допълнителни разходи за поддържане на брояча. Въпреки това, за ограничаване на паралелизма или реализация на модела „производител-потребител“, семафорът е незаменим.
| Характеристика | Semaphore | Mutex |
|---|---|---|
| Собственост | няма собственик | има собственик |
| Освобождаване | всяка нишка | само нишката-собственик |
| Брояч | от 0 до N | двоичен |
| Употреба | ограничаване на паралелизма и сигнализация | защита на критична секция |
| Рекурсия | не | да (reentrant) |
В разработката на мобилни приложения Semaphore се използва за управление на достъпа до ограничени ресурси: мрежови връзки, файлове, бази данни и хардуерни компоненти. Съвременните платформи предоставят удобни вградени реализации.
Един от типичните случаи — пул от HTTP връзки. Приложението може едновременно да изпраща не повече от 4 заявки към сървъра, тъй като API на доставчика ограничава паралелизма. Семафор с начална стойност 4 гарантира, че при всяко натоварване броят на едновременните заявки няма да надхвърли лимита, а останалите нишки ще чакат на опашка.
Без семафор, при рязко увеличение на потребителската активност, сървърната инфраструктура може да претърпи внезапно претоварване, което води до тайм-аути и грешки 429 Too Many Requests. Семафорът работи като предпазител, пропускайки строго определен брой едновременни извиквания, независимо от броя на активните нишки.
Android предоставя класа Semaphore от пакета java.util.concurrent. Нека разгледаме пример за ограничаване на едновременните мрежови заявки до две нишки за предотвратяване на претоварване на сървъра.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
В iOS DispatchSemaphore от GCD решава същата задача. Разработчиците го използват за синхронизиране на достъпа до ресурси в асинхронен код без блокиране на основната нишка.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
Най-честата грешка — забравен release при изключение. Ако нишка завърши с грешка преди извикване на release, семафорът завинаги остава блокиран за останалите нишки. Използвайте try/finally или defer за гарантирано освобождаване. Вторият проблем е deadlock при завладяване на няколко семафора в различен ред от различни нишки.
Семафорите се прилагат не само за защита на данни, но и за координация на нишки в сложни многонишкови сценарии. Познаването на често срещаните модели ускорява разработката и намалява вероятността от грешки при синхронизация.
Съществуват няколко доказани модела за прилагане на семафори в реални проекти. Познаването им помага да се избегнат типични грешки и да се изградят надеждни многонишкови системи.
Семафор с начална стойност N и периодичен release чрез таймер реализира ограничаване на скоростта на заявките към API. Например услугата позволява 10 заявки в секунда: семафорът стартира с 10, всяка заявка намалява брояча, а отделен TimerTask веднъж в секунда връща брояча към началната стойност. Това защитава както приложението, така и сървъра от претоварване.
В класическата задача производител-потребител два семафора управляват буфера: empty (разрешения за запис) и full (разрешения за четене). Производителят извиква acquire на empty и release на full, Потребителят — обратно. Тази схема гарантира, че Потребителят никога няма да прочете празен буфер, а Производителят няма да го препълни.
Същата схема е в основата на ограничения буфер в операционните системи — цикличен буфер с фиксиран размер. В мобилни приложения този модел се използва за обработка на опашки от изображения, видео файлове и аналитични събития.
Семафорите се използват успешно за throttling на мрежови повиквания във фонов режим. Например аналитично приложение изпраща пакети със събития към сървъра. Без ограничаване на едновременните нишки при пикови натоварвания (стартиране на приложението, синхронизация след офлайн) броят на едновременните заявки може да надхвърли лимитите на сървъра. Семафор с начална стойност 3 гарантира плавно изпращане и предотвратява блокиране от страна на сървъра.
Често задавани въпроси
Семафорът не е просто брояч, а примитив за синхронизация с атомарни операции и опашка за изчакване. Обикновеният брояч не блокира нишката и не гарантира атомарност на увеличението при конкурентен достъп от множество нишки.
Да, deadlock е възможен при завладяване на няколко семафора в различен ред от различни нишки. Например нишка A завладява S1, след това S2, а нишка B — S2, след това S1. Определете единен ред на завладяване за всички семафори в проекта.
Нишката се блокира и преминава в състояние на изчакване. Тя не консумира процесорно време, докато друга нишка не извика release. В Java това е състояние BLOCKED, в Swift нишката се спира от GCD.
Основната разлика — собственост. Mutex може да бъде освободен само от нишката-собственик. Binary Semaphore може да бъде освободен от всяка нишка, което е удобно за сигнализация между нишки, но по-малко безопасно за защита на целостта на данните.
Началната стойност зависи от сценария. За защита на един ресурс — 1. За пул от N връзки — N. За сигнализация между нишки използвайте 0, така че нишката-потребител да чака сигнал от производителя.
Заключение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също