2

I'm running into some strange errors with initialising Locks and running asynchronous code. Suppose we had a class to use with some resource protected by a lock.

import asyncio

class C:
    def __init__(self):
        self.lock = asyncio.Lock()

    async def foo(self):
        async with self.lock:
            return 'foo'

def async_foo():
    c = C()
    asyncio.run(c.foo())

if __name__ == '__main__':
    async_foo()
    async_foo()

This throws an error when run. It occurs on lock initialisation in init.

RuntimeError: There is no current event loop in thread 'MainThread'.

So duplicating the asyncio.run call in the function does not have this effect. It seems that the object needs to be initialised multiple times. It is also not enough to instantiate multiple locks in a single constructor. So perhaps it has something to do with the event loops state after asyncio.run is called.

What is going on? And how could I modify this code to work? Let me also clarify a bit, the instance is created outside asyncio.run and async functions for a reason. I'd like for it to be usable elsewhere too. If that makes a difference.

Alternatively, can threading.Lock be used for async things also? It would have the added benefit of being thread-safe, which asyncio.Lock reportedly is not.

Felix
  • 2,548
  • 19
  • 48

1 Answers1

2

What is going on?

  • When async object is created (asyncio.Lock()) it is attached to current event loop and can only be used with it
  • Main thread have some default current event loop (but other threads you create won't have default event loop)
  • asyncio.run() internally creates new event loop, set it current and close it after finished

So you're trying to use lock with event loop other than one it was attached to on creation. It leads to errors.

And how could I modify this code to work?

Ideal solution is following:

import asyncio


async def main():
    # all your code is here


if __name__ == "__main__":
    asyncio.run(main())

This will guarantee that every async object created is attached to proper event loop asyncio.run has created.

Running event loop (inside asyncio.run) is meant to be global "entry point" of your async program.

I'd like for it to be usable elsewhere too.

You're able to create an object outside asyncio.run, but then you should you should move creating async object from __init__ somewhere elsewhere so that asyncio.Lock() wouldn't be created until asyncio.run() is called.

Alternatively, can threading.Lock be used for async things also?

No, it is used to work with threads, while asyncio operates coroutines inside a single thread (usually).

It would have the added benefit of being thread-safe, which asyncio.Lock reportedly is not.

In asyncio you usually don't need threads other than main. There're still some reasons to do it, but thread-unsafety of asyncio.Lock shouldn't be an issue.


Consider reading following links. It may help to comprehend a situation better:

Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
  • Thank you! The first part clarifies things a lot. But, is using threading completely out of the question? I mean, yes, it is intended for multiple threads, but why not a single thread? If a resource is to be protected, surely a more generic lock is enough for a specific situation. If I acquire it, will it not protect the resource from other concurrent calls? Or is it the coroutine nature that prevents async from working right in that situation? - Ah, I think the second answer of the question you linked addresses that issue. – Felix Feb 02 '20 at 15:31