Async Locks and asyncio.Lock()
Stryg for at vise menuen
The asyncio event loop runs in a single thread, so two coroutines never execute at the exact same moment. But they can still interleave – one coroutine can be interrupted at any await point, and another can modify shared state before the first resumes. asyncio.Lock() prevents this.
The Problem: Interleaved State Mutation
Without a lock, shared state can become inconsistent when multiple coroutines modify it concurrently:
12345678910111213141516171819202122232425import asyncio import nest_asyncio nest_asyncio.apply() shared_counter = 0 # Incrementing a shared counter without a lock async def increment(label): global shared_counter current_value = shared_counter await asyncio.sleep(0) # Yielding control – another coroutine can run here shared_counter = current_value + 1 print(f"{label}: counter = {shared_counter}") async def main(): await asyncio.gather( increment("task_A"), increment("task_B"), increment("task_C"), ) print(f"Final counter: {shared_counter}") # Expected 3, may get 1 asyncio.run(main())
await asyncio.sleep(0) yields control without actually sleeping. At that point, another coroutine reads the same stale value and overwrites it.
Using asyncio.Lock()
A lock ensures only one coroutine can execute the protected block at a time:
123456789101112131415161718192021222324252627import asyncio import nest_asyncio nest_asyncio.apply() shared_counter = 0 lock = asyncio.Lock() # Incrementing a shared counter safely with a lock async def increment(label): global shared_counter async with lock: # Acquiring the lock current_value = shared_counter await asyncio.sleep(0) # Safe – no other coroutine can enter the lock shared_counter = current_value + 1 print(f"{label}: counter = {shared_counter}") async def main(): await asyncio.gather( increment("task_A"), increment("task_B"), increment("task_C"), ) print(f"Final counter: {shared_counter}") # Always 3 asyncio.run(main())
The async with lock block is entered by only one coroutine at a time. Any other coroutine that tries to acquire the lock waits until it is released.
A Realistic Example: Shared Cache
Locks are commonly used to protect shared data structures like caches:
1234567891011121314151617181920212223242526272829303132333435363738import asyncio import httpx import nest_asyncio nest_asyncio.apply() cache = {} cache_lock = asyncio.Lock() # Fetching a post with a shared cache protected by a lock async def fetch_post_cached(client, post_id): async with cache_lock: if post_id in cache: print(f"Cache hit for post {post_id}") return cache[post_id] response = await client.get( f"https://jsonplaceholder.typicode.com/posts/{post_id}" ) title = response.json()["title"] async with cache_lock: cache[post_id] = title return title async def main(): async with httpx.AsyncClient() as client: results = await asyncio.gather( fetch_post_cached(client, 1), fetch_post_cached(client, 1), # Second request hits cache fetch_post_cached(client, 2), ) for result in results: print(result) asyncio.run(main())
When to Use Locks
Locks are needed when:
- Multiple coroutines read and then write the same variable;
- A check-then-act sequence must be atomic (e.g., cache lookup then insert);
- A shared data structure (list, dict) is modified by more than one coroutine.
Locks are not needed for read-only access to shared data – concurrent reads are always safe.
Tak for dine kommentarer!
Spørg AI
Spørg AI
Spørg om hvad som helst eller prøv et af de foreslåede spørgsmål for at starte vores chat