Search code examples
pythonpython-asyncio

How to garbage collect Python background asyncio tasks?


I have a python class that uses per instance worker asyncio tasks as shown below.

What happens when an instance of this class is garbage collected, will its asyncio tasks will be garbage collected at 'about the same time'?

I read somewhere about global weak references to asyncio tasks and am not sure if I need to be more proactive about stopping them, e.g. in a finalizer of the MyClass object that contains them (?). I am targeting the latest stable python version and compatibility with older python versions is not an issue.

import asyncio

class MyClass:
  def __init__(self):
    self.__background_tasks = []
    for i in range(3):
      task = asyncio.create_task(self.my_task_body())
      self.__background_tasks.append(task)

  async def my_task_body(self):
    while True:
      # do some work here

Solution

  • Yes, that is the way Python works: if the only hard-reference to an object you keep is in one object, when that object is de-referenced, so are the others it references, recursively. When the reference count for any of these gets to zero, they are deleted.

    There is no need to worry about explicit garbage collection in code like this. And even the automatic "garbage colector" would only be needed if there was a cyclic reference across those objects, preventing the ref-count of any to reach 0. For straightforward, non cyclic references, object deletion is deterministic and synchronous.

    The difference for asyncio is that while a task is being executed, the asyncio loop creates temporary hard-references to them - so, if you want them to stop execution, you should cancel all remaing tasks explicitly. One point to do that is on your class' __del__ method:

    import asyncio
    
    class MyClass:
        def __init__(self):
            self.__background_tasks = set()
            for i in range(3):
            task = asyncio.create_task(self.my_task_body())
            task.add_done_callback(self.task_done)
            self.__background_tasks.add(task)
    
        def task_done(self, task):
            self.__background_tasks.remove(task)
            # optionally get task.result()/.exception() and record data
            ...
    
        async def my_task_body(self):
            while True:
                # do some work here
                ...
        
        def __del__(self):
            for task in self.__background_tasks:
                if not task.done():
                    task.cancel() 
    

    (note that adding the callback will create the cyclic references I mentioned above, as the callbacks are methods bound to the instance of MyClass. Nothing to worry: when that instance is de-referenced in the "outside world", it will be deleted - this time by the garbage collector, instead of the determistic deletion when ref-counts reach 0)