Semaphore is a synchronization primitive that controls access to a shared resource through a counter and a queue of waiting threads. According to Wikipedia, 2024, the semaphore was proposed by Edsger Dijkstra in 1965 to solve multithreaded interaction problems. The tool allows limiting the number of threads simultaneously working with a critical section.
Key Takeaways
Semaphore is a synchronization primitive that uses a counter to control access to a shared resource. The concept was proposed by Edsger Dijkstra in 1965 and became the foundation for all modern synchronization mechanisms in operating systems.
A semaphore is an integer variable with two atomic operations: wait (acquire) and signal (release). The wait operation decreases the counter, while signal increases it. When the counter reaches zero, the thread calling wait is blocked until signal is executed by another thread.
The main purpose of a semaphore is to protect critical sections from simultaneous access by multiple threads. Unlike a mutex, a semaphore does not require binding to an owner thread, making it suitable for a wider range of coordination tasks.
The concept of the semaphore originated in the context of the THE operating system, developed at the Technische Hogeschool Eindhoven. Dijkstra formalized the semaphore as a mathematical abstraction, proving its sufficiency for implementing any synchronization primitives.
The semaphore mechanism is based on two atomic operations and an internal wait queue. When acquire is called, the thread checks the counter value and either continues execution or blocks until the resource is released.
When creating a semaphore, an initial value of the permission counter is set. Each acquire call decreases the counter by 1. If the counter becomes negative after this, the thread is blocked. The release operation increases the counter and wakes up one of the waiting threads.
import java.util.concurrent.Semaphore
val semaphore = Semaphore(3)
fun accessResource() {
semaphore.acquire()
try {
println("${Thread.currentThread().name} is working")
} finally {
semaphore.release()
}
}
When a thread calls acquire with a zero counter, the OS places it in the semaphore's FIFO queue. The thread transitions to the BLOCKED state, consuming no CPU time. After a release call, the first thread in the queue transitions to the RUNNABLE state and gains access to the resource.
In synchronization theory, two main types of semaphores are distinguished: binary and counting. The choice of type depends on the specific task of managing access to resources.
A binary semaphore only takes values 0 and 1. In behavior, it resembles a mutex, but without the ownership requirement — any thread can perform a release. Such semaphores are convenient for implementing readiness flags and events between threads.
val ready = Semaphore(0)
fun producer() {
Thread.sleep(1000)
ready.release()
}
fun consumer() {
ready.acquire()
println("Data ready")
}
A counting semaphore can take any non-negative value. It is used to manage a pool of similar resources where multiple instances are available. For example, a pool of 5 network connections: each acquire takes one connection, release returns it to the pool.
Counting semaphores are indispensable for rate-limiting access to external services and implementing thread pools. They allow precise control of the degree of parallelism without manual thread management.
| Parameter | Binary Semaphore | Counting Semaphore |
|---|---|---|
| Range | 0 or 1 | 0 to N |
| Concurrent Threads | 1 | up to N |
| Usage | signaling, flags | resource pools, rate limiting |
Developers often confuse semaphore and mutex, although there are fundamental differences between them. Understanding these differences is critically important for choosing the right synchronization mechanism in a project.
The key difference is the ownership concept. A mutex always knows which thread has acquired it, and only that thread can release it. A semaphore has no owner: any thread can call release without even calling acquire. This makes a mutex safer for data protection and a semaphore more flexible for coordination.
In practice, a mutex is faster for simple mutual exclusion thanks to optimizations for typical scenarios. A semaphore requires additional overhead for maintaining the counter. However, for limiting parallelism or implementing the producer-consumer pattern, a semaphore is indispensable.
| Characteristic | Semaphore | Mutex |
|---|---|---|
| Ownership | no owner | has an owner |
| Release | any thread | only owner thread |
| Counter | 0 to N | binary |
| Use case | limiting parallelism and signaling | critical section protection |
| Recursion | no | yes (reentrant) |
In mobile application development, Semaphore is used to manage access to limited resources: network connections, files, databases, and hardware components. Modern platforms provide convenient built-in implementations.
One typical use case is an HTTP connection pool. An application may send no more than 4 simultaneous requests to a server because the provider's API limits parallelism. A semaphore with an initial value of 4 ensures that under any load, the number of concurrent requests does not exceed the limit, while other threads wait in the queue.
Without a semaphore, a sharp increase in user activity could cause sudden overload on the server infrastructure, leading to timeouts and 429 Too Many Requests errors. Semaphore acts as a fuse, allowing strictly a specified number of concurrent calls regardless of the number of active threads.
Android provides the Semaphore class from the java.util.concurrent package. Let's look at an example of limiting concurrent network requests to two threads to prevent server overload.
class ApiClient {
private val throttle = Semaphore(2)
suspend fun fetch(url: String): Result {
throttle.acquire()
return try {
httpGet(url)
} finally {
throttle.release()
}
}
}
In iOS, DispatchSemaphore from GCD solves the same task. Developers use it to synchronize access to resources in asynchronous code without blocking the main thread.
let semaphore = DispatchSemaphore(value: 3)
func processBatch(_ items: [UIImage]) {
for img in items {
semaphore.wait()
DispatchQueue.global().async {
applyFilter(to: img)
semaphore.signal()
}
}
}
The most common mistake is a forgotten release when an exception occurs. If a thread terminates with an error before calling release, the semaphore remains permanently blocked for other threads. Use try/finally or defer to guarantee release. The second problem is deadlock when acquiring multiple semaphores in different orders by different threads.
Semaphores are used not only for data protection but also for thread coordination in complex multithreaded scenarios. Knowing common patterns speeds up development and reduces the likelihood of synchronization errors.
There are several proven patterns for using semaphores in real-world projects. Knowing them helps avoid typical mistakes and build reliable multithreaded systems.
A semaphore with an initial value of N and periodic release via a timer implements rate limiting for API requests. For example, a service allows 10 requests per second: the semaphore starts at 10, each request decreases the counter, and a separate TimerTask returns the counter to its initial value every second. This protects both the application and the server from overload.
In the classic producer-consumer problem, two semaphores manage a buffer: empty (write permissions) and full (read permissions). The Producer calls acquire on empty and release on full, while the Consumer does the opposite. This scheme guarantees that the Consumer never reads an empty buffer and the Producer never overflows it.
This same scheme underlies the bounded buffer in operating systems — a ring buffer with a fixed size. In mobile applications, the pattern is used for processing queues of images, video files, and analytics events.
Semaphores are successfully used for throttling network calls in background services. For example, an analytics application sends event packets to the server. Without limiting concurrent threads during peak loads (app launch, synchronization after offline), the number of simultaneous requests can exceed server limits. A semaphore with an initial value of 3 ensures smooth sending and prevents server-side blocking.
Frequently Asked Questions
Semaphore is not just a counter, but a synchronization primitive with atomic operations and a wait queue. A regular counter does not block a thread and does not guarantee atomic increment when accessed concurrently by multiple threads.
Yes, deadlock is possible when acquiring multiple semaphores in different orders by different threads. For example, thread A acquires S1, then S2, while thread B acquires S2, then S1. Fix a single acquisition order for all semaphores in the project.
The thread blocks and enters a waiting state. It does not consume CPU time until another thread calls release. In Java, this is the BLOCKED state; in Swift, the thread is suspended by GCD.
The main difference is ownership. A Mutex can only be released by the owner thread. A Binary Semaphore can be released by any thread, which is convenient for signaling between threads but less safe for protecting data integrity.
The initial value depends on the scenario. For protecting a single resource — 1. For a pool of N connections — N. For signaling between threads, use 0 so that the consumer thread waits for a signal from the producer.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also