Python Central HubSign in

Python Programming / Python Asyncio

Asyncio Synchronization (Lock, Semaphore)

Coordinate async tasks with asyncio.Lock and asyncio.Semaphore to protect shared state and limit concurrency.

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.

Why async locks exist

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.

asyncio.Lock

Python
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())

asyncio.Semaphore (limit concurrency)

Python
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.

Check yourself

quizCheck yourself
  1. Twelve jobs run under `asyncio.Semaphore(4)`. What was the peak number in flight?

    Show answer

    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?

    Show answer

    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?

    Show answer

    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?

    Show answer

    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.

🧪 Try It Yourself

Exercise 1 – asyncio.Lock

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 – asyncio.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 3 – asyncio.Event

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