Skip to content

Asyncio Synchronization (Lock, Semaphore)

diagram an async semaphore limits concurrency without blocking a thread mermaid
There is one thread, so a task waiting for a permit must not block -- it suspends, and the loop runs something else. That is the difference from a threading semaphore: the ceiling is the same, but waiting is free. The permit is released by leaving the async with block, including when the body raises.
diagram an asyncio.Lock protects across awaits, which is the whole point mermaid
Ordinary async code needs no lock, because control only changes hands at an await you can see. A lock is needed exactly when a critical section CONTAINS an await -- read, await, write -- because another task can run in that gap. The lock makes the whole section atomic with respect to other tasks.

Even in a single-threaded event loop, tasks can interleave at await points.

So if a task:

  • reads shared state
  • awaits
  • writes shared state

…another task can run in-between.

async_lock.py
import asyncio
 
 
lock = asyncio.Lock()
count = 0
 
 
async def inc():
    global count
    for _ in range(10_000):
        async with lock:
            count += 1
 
 
async def main():
    await asyncio.gather(inc(), inc())
    print(count)
 
 
asyncio.run(main())
async_semaphore.py
import asyncio
 
sem = asyncio.Semaphore(3)
 
 
async def worker(i: int):
    async with sem:
        print("start", i)
        await asyncio.sleep(0.2)
        print("end", i)
 
 
async def main():
    await asyncio.gather(*(worker(i) for i in range(10)))
 
 
asyncio.run(main())
sketch A semaphore caps how many coroutines are inside at once p5.js
Twelve jobs of 0.05 seconds each, run under three different permit counts. The ceiling was never exceeded in any run -- peak in flight was exactly the limit. What changes is how many waves the work takes, and the elapsed times run slightly over the ideal because every wave has to be scheduled.
pch.quizTag pch.quizDefaultTitle
  1. Twelve jobs run under `asyncio.Semaphore(4)`. What was the peak number in flight?

    pch.quizShowAnswer

    B — 4 — Measured at exactly 4. The ceiling is a hard guarantee, not an average — and the same held for limits of 2 and 12.

  2. Why must you not use `threading.Semaphore` inside an event loop?

    pch.quizShowAnswer

    B — It blocks the whole thread, including the tasks holding the permits it is waiting for — That is a deadlock rather than a slowdown. `asyncio.Semaphore` suspends the waiting task and lets the loop run others, so a permit can actually be released.

  3. When is an `asyncio.Lock` actually needed?

    pch.quizShowAnswer

    B — Only when the critical section CONTAINS an `await` — There is one thread and control only changes hands at an `await`. Code between awaits cannot be interrupted, so it needs no lock; a read-await-write sequence does.

  4. The permit is released when?

    pch.quizShowAnswer

    B — On leaving the `async with` block, including when the body raises — That is the point of the context-manager form — an exception inside the body cannot leak a permit and starve everyone else.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading