Semaphore
· 535 words · 3 min read
What on Earth is a semaphore?
My coding agent fixed a memory leak in a web app I built, then mentioned it needed to move a section of code into a “semaphore.” I had no clue what a semaphore even meant, so here we are. I’m a little embarrassed to say I never encountered the term before, but I haven’t done much low-level programming–and when I did, with Arduino, I never worried about concurrency.
Edsger W. Dijkstra introduced the term semaphore to computer science in the early 1960s. He used it to solve the problem of process synchronization and mutual exclusion (although a semaphore differs from a mutex) in concurrent computing.
Before its adoption in programming, a semaphore (derived from the Greek sēma meaning “sign” and phoros meaning “bearer”) referred to a visual signaling apparatus.
A relief of the Greek hydraulic telegraph of Aeneas. Source: Wikipedia
Early 19th-century optical telegraph systems used pivoting arms or flags to transmit messages across distances. Mechanical railway trackside signals featured pivoting blades or arms (later colored lights) used to control train movements. These devices later shaped modern traffic signals.
While developing an operating system in the 1960s, Dijkstra confronted the challenge of mutual exclusion—ensuring that multiple concurrent processes or threads could safely share a resource or critical section of code without interfering with each other. He drew a direct analogy from railway traffic control: Imagine a stretch of single-track railway where only one train can safely pass at a time. A trackside semaphore guards that section of track. If the signal indicates danger/busy, any approaching train must wait until the signal changes. In software, Dijkstra translated this into a shared integer variable coupled with atomic operations. A thread checks or waits on this counter before entering a critical section, much like a train waiting on a signal arm.

P.S. #
Below, you’ll find the section of Python code including a semaphore from a little FastAPI app I built to automate a work task, in case you’re curious. It batch-processes images, uploads them to a multimodal AI model endpoint for alt text generation, allows user edits, and ultimately offers a downloadable .zip archive of the original image files with the alt text written to the EXIF metadata. The semaphore is dual-purpose: it keeps the app from consuming too many host system resources per process, and it prevents concurrent request rate limits from the LiteLLM model provider it connects to.
1semaphore = asyncio.Semaphore(max(1, settings.ai_concurrency))
2
3async def handle(image: JobImage) -> None:
4 source = originals / image.stored_name
5 try:
6 async with semaphore:
7 # The 3 lines below were executing outside the semaphore previously.
8 # Just after 'try:' All the images were decoded concurrently
9 # up to Python's default thread-pool size. Each task held its
10 # resized bytes in memory while waiting for the semaphore. glibc
11 # then doesn't return the freed heap to the OS, so the uvicorn
12 # server process (which runs forever) stayed at ~4GB.
13 data, media_type = await asyncio.to_thread(
14 imaging.resize_for_model, source, settings.model_max_dimension
15 )
16 image.alt_text = await provider.describe(data, media_type, prompt)
17 except Exception as exc:
18 image.error = f"{type(exc).__name__}: {exc}"
19
20await asyncio.gather(*(handle(image) for image in images))