The asyncio.Lock class only provides an async def acquire() method, but because it is async its very hard to acquire the lock from a normal function, because acquire only locks when it is awaited, which is only possible in a coroutine.
StreamWriter.write isn’t async, so it can be used to send data immedietly if called from a normal function, but in a non-coroutine it is very hard to protect access to the writer with a asyncio.Lock, which I would consider a common requirement.
I think something like a try_acquire() function, that is not async, would be a good addition.
I imagine the try_acquire function to be used like this:
writer: asyncio.StreamWriter = ...
command_in_flight_lock = asyncio.Lock()
async def wait_for_response():
await writer.drain()
...
command_in_flight_lock.release()
def send_command(cmd):
if command_in_flight_lock.try_acquire():
# We can send immedietly without waiting and locked the lock immedietly
writer.write(cmd)
return asyncio.create_task(wait_for_response())
# We couldn't acqurie the lock immedietly, so there is already a command in flight,
# so we have to wait for the lock until we can send another.
async def wait_and_send():
await command_in_flight_lock.acquire()
writer.write(cmd)
return wait_for_response()
return asyncio.create_task(wait_and_send())
This way the command can be send immedietly, when the lock can be acquired, and awaiting the response can be delayed, which would not be possible if send_command was a coroutine:
r = send_command("command")
work() # doing something else
await r
I think this function could easily be implemented by moving these lines of asyncio.Lock.acquire into another function.
I’m not a fan of the code duplication, but I also can’t think of another way around the “coroutines actually don’t start until they are awaited”, so not using coroutines seems like the best option.
Another downside of this solution is, that try_acquire didn’t reserve a spot in waiters list and if we run into the second case the waiter would only be registered when the await is reached, which might result in different ordering? Not sure if that is important.
That could maybe be solved with a different function that registers a waiter and returns an Optional<Future> that resolves when the Lock is acquired by the current caller?
Similar methods would probably also made sense for Condition and Semaphore
What do you think?