InfoLearner
InfoLearner

Reputation: 15598

Python: How to not get a deadlock for this simple functionality?

I have implemented a simple class called Saver that has two functions that its callers can execute. They are save_all() and save_one()

As the name implies, when I execute save_all(), I want it to execute the function save_one() in a loop. I just want to ensure that only one caller can execute either save_all() or save_one() at a given time from outside.


import asyncio

class Saver:
  def __init__(self):
    # it's lock
    self.lock = asyncio.Lock()


  def save_all():
     yield from self.lock.acquire()
     for i in range(3):
        save_one(i)
     self.lock.release()

  def save_one():
     yield from self.lock.acquire()
     print(i)
     self.lock.release()

Now what happens is that if I call the save_all(), it acquires the lock, and the save_one() print() statement is never executed.

How do I ensure that only caller can call either save_all() or save_one() from outside:

saver = Saver()
saver.save_all()

Upvotes: 1

Views: 790

Answers (1)

user4815162342
user4815162342

Reputation: 154996

The print in save_one() is not executed in the code as shown because the code never awaits save_one(), it just calls it. (But the code has other trivial flaws, so perhaps it's meant more like pseudo-code.) Also note that yield-from coroutines are deprecated, so you should use async def and await in place of def and yield from when writing async Python.

Since asyncio doesn't come with a reentrant lock, the simplest way to fix the issue is to lock only in API entry points. The underlying implementation methods can completely avoid locking, it being taken care of at the system boundaries. Then there is no deadlock because the internal _save_all will call the internal _save_one, and the lock will be acquired only once (in save_call, before invoking _save_all, or in save_one, before invoking _save_one, as the case may be):

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

    async def save_all(self):
        async with self.lock:
            await self._save_all()

    async def save_one(self):
        async with self.lock:
            await self._save_one()

    async def _save_all(self):
        # private method, must be invoked with the lock held
        for i in range(3):
            await self._save_one(i)

    async def _save_one(self, i):
        # private method, must be invoked with the lock held
        print(i)

Upvotes: 3

Related Questions