Asyncio Synchronization (Lock, Semaphore)
flowchart TD
A["async with sem:"] --> B{"a permit free?"}
B -->|yes| C["take it, run the body"]
B -->|no| D["SUSPEND -- the loop runs other tasks"]
D --> E["a permit is released"]
E --> C
C --> F["body awaits I/O"]
F --> G["leaving the block releases the permit"]
G --> H["even if the body raised"]
H --> I["a waiting task is resumed"]
flowchart TD
A["task A: async with lock"] --> B["reads shared state"]
B --> C["await -- control goes to the loop"]
C --> D{"task B wants the lock"}
D --> E["B suspends; it does not enter"]
C --> F["A resumes, writes, leaves the block"]
F --> G["lock released"]
G --> H["B enters and runs the whole section"]
I["no await inside the section?"] --> J["you do not need a lock at all"]
Why async locks exist
Section titled “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
Section titled “asyncio.Lock”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)
Section titled “asyncio.Semaphore (limit concurrency)”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())Check yourself
Section titled “Check yourself”-
Twelve jobs run under `asyncio.Semaphore(4)`. What was the peak number in flight?
Measured at exactly 4. The ceiling is a hard guarantee, not an average — and the same held for limits of 2 and 12.
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.
-
Why must you not use `threading.Semaphore` inside an event loop?
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.
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.
-
When is an `asyncio.Lock` actually needed?
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.
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.
-
The permit is released when?
That is the point of the context-manager form — an exception inside the body cannot leak a permit and starve everyone else.
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.
🧪 Try It Yourself
Section titled “🧪 Try It Yourself”Exercise 1 – asyncio.Lock
Section titled “Exercise 1 – asyncio.Lock”Exercise 2 – asyncio.Semaphore
Section titled “Exercise 2 – asyncio.Semaphore”Exercise 3 – asyncio.Event
Section titled “Exercise 3 – asyncio.Event”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading