Да, замки еще нужны. Одновременная модификация небезопасна только потому, что она происходит через сопрограммы вместо потоков.
asyncio имеет свой собственный выделенный asyncio.Lock
, а также свои собственные версии других примитивов синхронизации, потому что блокировка, которая заботится о потоках, не будет защищать сопрограммы друг от друга, и ожидание блокировки должно происходить через событие l oop, а не за счет блокировки потока.
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)
...
Это сказал, поскольку сопрограммы основаны на совместной многозадачности, а не на вытеснении, вы иногда можете гарантировать, что блокировки не нужны в тех случаях, когда они будут необходимы для потоков. Например, если нет точек, в которых любая сопрограмма могла бы передать управление во время критического раздела, тогда вам не нужна блокировка.
Например, для этого нужна блокировка:
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])
Технически этого не происходит:
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])
, но без блокировки рискует ввести ошибки по мере изменения кода, особенно потому, что следующий требует блокировки, в обе сопрограммы:
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])
Без блокировок в обеих сопрограммах coro2
может прервать coro1
, а coro1
требует монопольного доступа к общему ресурсу.