Semaphore: what it is, how it works, and its use in thread synchronization

Author: IT Sectr Published: 2026-03-19 Reading time: 8 min

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 controls access through a permission counter.
  • Binary semaphore takes values 0 and 1, acting as a blocking flag.
  • Counting semaphore allows simultaneous access for a specified number of threads.
  • Unlike a mutex, a semaphore is not tied to an owner thread.
  • Deadlock is one of the main dangers when using semaphores incorrectly.

What is a Semaphore?

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.

Definition and Purpose

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.

History and Theoretical Foundation

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.

How Does a Semaphore Work?

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.

Counter and Atomic Operations

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.

kotlin
import java.util.concurrent.Semaphore

val semaphore = Semaphore(3)

fun accessResource() {
    semaphore.acquire()
    try {
        println("${Thread.currentThread().name} is working")
    } finally {
        semaphore.release()
    }
}

Wait Queue and Scheduling

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.

Types of Semaphores

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.

Binary Semaphore

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.

kotlin
val ready = Semaphore(0)

fun producer() {
    Thread.sleep(1000)
    ready.release()
}

fun consumer() {
    ready.acquire()
    println("Data ready")
}

Counting Semaphore

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.

ParameterBinary SemaphoreCounting Semaphore
Range0 or 10 to N
Concurrent Threads1up to N
Usagesignaling, flagsresource pools, rate limiting

Semaphore vs Mutex

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.

Ownership Principle

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.

Performance and Use Cases

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.

CharacteristicSemaphoreMutex
Ownershipno ownerhas an owner
Releaseany threadonly owner thread
Counter0 to Nbinary
Use caselimiting parallelism and signalingcritical section protection
Recursionnoyes (reentrant)

Semaphore in Mobile Development

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.

Server Connection Pool

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.

Semaphore in Kotlin for Android

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.

kotlin
class ApiClient {
    private val throttle = Semaphore(2)

    suspend fun fetch(url: String): Result {
        throttle.acquire()
        return try {
            httpGet(url)
        } finally {
            throttle.release()
        }
    }
}

DispatchSemaphore in Swift for iOS

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.

swift
let semaphore = DispatchSemaphore(value: 3)

func processBatch(_ items: [UIImage]) {
    for img in items {
        semaphore.wait()
        DispatchQueue.global().async {
            applyFilter(to: img)
            semaphore.signal()
        }
    }
}

Common Mistakes When Working with Semaphores

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.

Semaphore Usage Patterns

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.

Rate Limiter

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.

Producer-Consumer with Semaphores

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.

Throttling Network Requests

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

What is the difference between a Semaphore and a regular counter?

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.

Can a semaphore cause a deadlock?

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.

What happens when acquire is called with a zero counter?

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.

How is Binary Semaphore fundamentally different from Mutex?

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.

What initial value should I choose for the counter?

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

  • Semaphore is a counter-based synchronization primitive proposed by Dijkstra in 1965.
  • Binary semaphore takes values 0 and 1; counting semaphore takes any non-negative value.
  • Acquire and release operations are atomic and thread-safe.
  • Unlike a mutex, a semaphore is not tied to an owner thread.
  • Counting semaphores are used for managing resource pools and limiting parallelism.
  • Forgotten release is the most common mistake leading to thread hanging.

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.

Discuss the project

Read also