Search code examples
pythonlockingpython-asyncio

Lock in Asyncio


I'm trying to use Lock in Asyncio to prevent the function method() to run "more that ones at the same time".

My motivation: I use the Asyncio library for asynchronous communication with multiple devices. The issue is, that the devices need some time to respond and if other request is send in the meantime to the same device, error occurs. So I want to built in the class (of the device) a Lock to prevent that.

This code is just very simple way to test the concept and understand it before implementing that to the actual code. BUT even this test code does not work as I expect it. :D

First: If I try to run this code:

import asyncio

async def method(wait:int):
  print(f"Method with waiting {wait}s starting")
  await asyncio.sleep(wait)
  print(f"Method with waiting {wait}s finished")

async def main():
  task1 = asyncio.create_task(method(2))
  task2 = asyncio.create_task(method(2))
  await task1
  await task2

asyncio.run(main())

The task1 starts execution, sleeps so the task2 starts. Then they both finish right after each other like this (which is to expect):

Method with waiting 2s starting
Method with waiting 2s starting
Method with waiting 2s finished
Method with waiting 2s finished

Now I want to prevent starting the execution of the task2 before the task1 finish. And the best way (I though) would be with Lock. So I adapted the code:

import asyncio
locker = asyncio.Lock()

async def method(wait:int):
  async with locker:
    print(f"Method with waiting {wait}s starting")
    await asyncio.sleep(wait)
    print(f"Method with waiting {wait}s finished")

async def main():
  task1 = asyncio.create_task(method(2))
  task2 = asyncio.create_task(method(2))
  await task1
  await task2

asyncio.run(main())

The code does not work as expected and I get following error:

Method with waiting 2s starting
Method with waiting 2s finished
Traceback (most recent call last):
  File "/Users/filip/Desktop/Python/07_Asyncio2.py", line 16, in <module>
    asyncio.run(main())
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 642, in run_until_complete
    return future.result()
  File "/Users/filip/Desktop/Python/07_Asyncio2.py", line 14, in main
    await task2
  File "/Users/filip/Desktop/Python/07_Asyncio2.py", line 5, in method
    async with locker:
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/locks.py", line 14, in __aenter__
    await self.acquire()
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/locks.py", line 120, in acquire
    await fut
RuntimeError: Task <Task pending name='Task-3' coro=<method() running at /Users/filip/Desktop/Python/07_Asyncio2.py:5>> got Future <Future pending> attached to a different loop

Running on MacOS, Python version: 3.9.5 Thank you for any help and tips :)


Solution

  • Change code the following way and it will work:

    import asyncio
    
    
    async def method(wait: int, locker: asyncio.Lock):
        async with locker:
            print(f"Method with waiting {wait}s starting")
            await asyncio.sleep(wait)
            print(f"Method with waiting {wait}s finished")
    
    
    async def main():
        locker = asyncio.Lock()
        await asyncio.gather(method(2, locker), method(2, locker))
    
    if __name__ == '__main__':
        asyncio.run(main())
    
    1. provide asyncio.Lock as function argument
    2. use asyncio.gather to run tasks simultaneously, the way you used tasks you do not need lock at all.

    The main problem is that your lock and you coroutines belongs to different event loops, so I moved lock to another place.

    How to check that loops are different? add line print(id(asyncio.get_event_loop())) after you lock declaration and at the begin of your main function.