Python Central HubSign in

Python Programming / Python Synchronization

Semaphores (limit concurrency)

Use Semaphore to limit concurrency (like a pool) and protect limited resources.

What is a semaphore?

A semaphore controls access to a limited resource.

Example: only 3 workers may access an API at once.

Example

Python
import threading
import time
 
sem = threading.Semaphore(3)
 
 
def worker(i: int) -> None:
    with sem:
        print("start", i)
        time.sleep(0.5)
        print("end", i)
 
 
threads = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()
sketch A semaphore caps how many run at once p5.js
Twelve jobs of 0.05 s each against a semaphore. The permit count is a hard ceiling on in-flight work, so the jobs go through in waves and the total time is roughly waves multiplied by job length. Measured: with 2 permits the peak in-flight was exactly 2 and the run took 0.31 s against a 0.30 s ideal; with 3 permits, peak 3 and 0.21 s against 0.20 s. Click to switch the permit count.

BoundedSemaphore

BoundedSemaphore raises an error if you release too many times.

Use it to catch bugs.

Python
import threading
 
sem = threading.BoundedSemaphore(1)
sem.acquire()
sem.release()
# sem.release()  # would raise ValueError

A counter with a waiting room

A Semaphore holds a number of permits. acquire() takes one, and if none are left the thread waits. release() gives one back. Unlike a Lock, which lets in exactly one thread, a Semaphore(3) lets in up to three. A lock is simply a semaphore with one permit.

Think of a cafe with three tables. New customers sit down while a table is free, and otherwise queue at the door. When someone leaves, the next person in line is seated.

Reading the example

Ten threads start, but only three can be inside with sem: at any moment. You will see three start lines immediately, then after about half a second three end lines, and the next three start lines follow as permits are released. The tenth thread runs alone in the final wave. The with block acquires on entry and releases on exit, even when an error happens inside.

A practical case: limiting API calls

Many web APIs refuse too many simultaneous requests. A semaphore keeps you inside their limit while still allowing some parallelism:

Python
import threading
import time
 
sem = threading.Semaphore(2)   # at most 2 downloads at once
active = 0
peak = 0
guard = threading.Lock()
 
 
def download(i: int) -> None:
    global active, peak
    with sem:
        with guard:
            active += 1
            peak = max(peak, active)
        time.sleep(0.2)        # pretend to download
        with guard:
            active -= 1
 
 
threads = [threading.Thread(target=download, args=(i,)) for i in range(6)]
for t in threads:
    t.start()
for t in threads:
    t.join()
print("most at once:", peak)  # 2

The counters, protected by a small lock, prove that the peak never exceeds two.

Semaphore versus BoundedSemaphore

A plain Semaphore lets you release more times than you acquired, silently raising the number of permits above the limit. That is nearly always a bug. BoundedSemaphore raises ValueError when this happens, so the mistake is caught immediately. Use it by default.

Common mistakes

  • Releasing without acquiring, which grows the permit count. Use with sem: and BoundedSemaphore.
  • Confusing a semaphore with a lock. A semaphore with more than one permit does not protect shared data from concurrent changes; you still need a lock for that.
  • Forgetting the release on an error path. Using with avoids this.
  • Setting the count too high, so it limits nothing.

When to use it

Use a semaphore to cap access to a limited resource: database connections, API rate limits, open files, or CPU-heavy sections that should not run all at once.

🧪 Try It Yourself

Exercise 1 – Create a Semaphore

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 2 – Semaphore as Context Manager

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Exercise 3 – BoundedSemaphore

exerciseYour turn

Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.

Output
Press Run to see what your code prints.

Was this page helpful?

Let us know how we did