Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do coroutines require locks when reading/writing a shared resource?

For example

shared = {}

async def coro1():
    # do r/w stuff with shared 

async def coro2():
    # do r/w stuff with shared 

asyncio.create_task(coro1())
asyncio.create_task(coro2())

If coro1 and coro2 both access a single dictionary/variable, both reading and writing, would it require some sort of mutex/lock? Or would it be fine since asyncio stuff only ever happens on 1 thread?

like image 913
psidex Avatar asked Sep 05 '25 03:09

psidex


1 Answers

Yes, you still need locks. Concurrent modification doesn't become safe just because it's happening through coroutines instead of threads.

asyncio has its own dedicated asyncio.Lock, as well as its own versions of other synchronization primitives, because a lock that cares about threads won't protect coroutines from each other, and waiting for a lock needs to happen through the event loop, not by blocking the thread.

shared = {}
lock = asyncio.Lock()

async def coro1():
    ...
    async with lock:
        await do_stuff_with(shared)
    ...

async def coro2():
    ...
    async with lock:
        await do_stuff_with(shared)
    ...

That said, since coroutines are based on cooperative multitasking instead of preemptive, you can sometimes guarantee that locks are unnecessary in cases where they would be necessary with threads. For example, if there are no points at which any coroutine could yield control during a critical section, then you don't need a lock.

For example, this needs a lock:

async def coro1():
    async with lock:
        for key in shared:
            shared[key] = await do_something_that_could_yield(shared[key])

async def coro2():
    async with lock:
        for key in shared:
            shared[key] = await do_something_that_could_yield(shared[key])

This technically doesn't:

async def coro1():
    for key in shared:
        shared[key] = do_something_that_cant_yield(shared[key])

async def coro2():
    for key in shared:
        shared[key] = do_something_that_cant_yield(shared[key])

but not locking risks introducing bugs as the code changes, particularly since the following does require locks, in both coroutines:

async def coro1():
    async with lock:
        for key in shared:
            shared[key] = await do_something_that_could_yield(shared[key])

async def coro2():
    async with lock:
        for key in shared:
            shared[key] = do_something_that_cant_yield(shared[key])

Without locks in both coroutines, coro2 could interrupt coro1 while coro1 needs exclusive access to the shared resource.

like image 85
user2357112 supports Monica Avatar answered Sep 07 '25 21:09

user2357112 supports Monica