0

I have created a class A using the following pattern

class A:
    def __init__(self):
        self.worker = threading.Thread(target=self.workToDo)
        self.worker.setDaemon(daemonic=True)
        self.worker.start()
    
    def workToDo(self):
        while True:
             print("Work")

However, this design gets not garbage collected. I assume that this is due to a circular dependency between the running thread and its parent.

How can i design a class that starts a periodic thread that does some work, stops this thread on destruction and gets destructed as soon as all obvious references to the parent object get out of scope.

I tried to stop the thread in the ___del___ method, but this method is never called (i assume due to the circular dependency).

Mehno
  • 868
  • 2
  • 8
  • 21
  • 1
    You could try giving the thread a WeakRef rather than a normal reference, to avoid the circular dependency – mousetail Jun 14 '22 at 08:52
  • 1
    You shouldn't rely on garbage collection for deterministic resource release in the first place. Handle it explicitly, with a `with` statement or something. – user2357112 Jun 14 '22 at 08:54
  • "However, this design gets not garbage collected." How exactly did you determine this? – Karl Knechtel Jun 14 '22 at 08:54
  • @user2357112 I do not really like to depend on the user of the class to call a stop method or a context manager. Also, the lifetime of that object is not limited to a certain context. – Mehno Jun 14 '22 at 08:57
  • @Mehno: Garbage collection is even *less* reliable than what you're worried about relying on. – user2357112 Jun 14 '22 at 09:05
  • BTW [this thread](https://stackoverflow.com/a/1481512/1185254) gives some useful background (as well as hints to indeed use the context manager) – alex Jun 14 '22 at 09:06
  • @user2357112 i still prefer a solution that has a certain amount of safety and does not require the developer to call a cleanup method. C++ would offer me something via RAII. I do not know whether there is a clean solution in python. – Mehno Jun 14 '22 at 10:31
  • @Mehno: `with` is Python's closest equivalent to RAII. Relying on garbage collection is *far* less safe. If you're coming from C++, it may be tempting to try to replicate C++ design patterns with `__del__`, but that's a really bad idea. Resource management in garbage-collected languages is very different from C++. Don't try to write C++ in Python. – user2357112 Jun 14 '22 at 10:37
  • @mousetail i updated my question with your suggestion. Thank you. :) – Mehno Jun 14 '22 at 11:23
  • @Mehno: Now your thread isn't actually doing the work. Setting `target` to a weak reference means the thread calls the *weak reference object*, rather than the method you wanted it to call. Calling a weak reference just returns the referent if it's alive, or None if the referent is dead. It doesn't actually execute `self.workToDo`. – user2357112 Jun 14 '22 at 12:12
  • @user2357112 you are right. I removed that option. I actually just observed, that del was called but did not observe whether the worker function runs. Thx – Mehno Jun 14 '22 at 12:19

2 Answers2

0

There is no circular dependence, and the garbage collector is doing exactly what it is supposed to do. Look at the method workToDo:

def workToDo(self):
        while True:
             print("Work")

Once you start the thread, this method will run forever. It contains a variable named self: the instance of class A that originally launched the thread. As long as this method continues to run, there is an active reference to the instance of A and therefore it cannot be garbage collected.

This can easily be demonstrated with the following little program:

import threading
import time

def workToDo2():
    while True:
        print("Work2")
        time.sleep(0.5)

class A:
    def __init__(self):
        self.worker = threading.Thread(target=workToDo2, daemon=True)
        self.worker.start()
    
    def workToDo(self):
        while True:
             print("Work")
             time.sleep(0.5)
             
    def __del__(self):
        print("del")

A()
time.sleep(5.0)

If you change the function that starts the thread from self.workToDo to workToDo2, the __del__ method fires almost immediately. In that case the thread does not reference the object created by A(), so it can be safely garbage collected.

Your statement of the problem is based on a false assumption about how the garbage collector works. There is no such concept as "obvious reference" - there is either a reference or there isn't.

The threads continue to run whether the object that launched them is garbage collected or not. You really should design Python threads so there is a mechanism to exit from them cleanly, unless they are true daemons and can continue to run without harming anything.

I understand the urge to avoid trusting your users to call some sort of explicit close function. But the Python philosophy is "we're all adults here," so IMO this problem is not a good use of your time.

Paul Cornelius
  • 9,245
  • 1
  • 15
  • 24
  • Thank you for your answer. I still do not understand why you start with "there is no circular dependency" and then you are describing exactly that. The thread holds a reference to the object via self and the object holds the thread. It seems to me that this is the circle. – Mehno Jun 14 '22 at 10:56
  • See Yilmaz's answer here: https://stackoverflow.com/questions/43889268/circular-reference-in-python for the case I consider typical (two defined objects pointing to each other). Circular references in Python are generally not a problem; they are handled just fine - unless one of the classes has a `__del__` method. Of course every thread must retain references to all the variables in its call stack, but this is handled automatically and is not normally called a circular dependence IMO. – Paul Cornelius Jun 15 '22 at 03:36
  • I would argue that a circular dependence is nothing that is linked to references but a logical construct and i used the term as such. I used the term obvious reference full aware that there is no such well defined object in the language but to state what my desired behavior is. C++ has a safe solution for such a problem that is tied to the lifetime of the object. I just wonder whether there is an elegant solution in python. I do not think that context manager, which i appreciate, help me with the situation at hand. – Mehno Jun 16 '22 at 11:55
-2

Syntax of destructor declaration:

def __del__(self):
  # body of destructor

Note: A reference to objects is also deleted when the object goes out of reference or when the program ends.

Example 1: Here is the simple example of destructor. By using del keyword we deleted the all references of object ‘obj’, therefore destructor invoked automatically

Python program to illustrate destructor:

class Employee:

    # Initializing
    def __init__(self):
        print('Employee created.')

    # Deleting (Calling destructor)
    def __del__(self):
        print('Destructor called, Employee deleted.')

obj = Employee()
del obj
martineau
  • 119,623
  • 25
  • 170
  • 301
  • 2
    Welcome to Stack Overflow. Please read [answer] and the [formatting help](https://stackoverflow.com/help/formatting). Make sure your code shows up with proper formatting, and use full English sentences - outside the code, don't just write comments - in order to explain *how* the code solves the problem and *why* the problem occurs. – Karl Knechtel Jun 14 '22 at 08:55
  • In Python, destructors **aren't** always called when you expect — see [Python destructor and garbage collection notes](https://www.electricmonk.nl/log/2008/07/07/python-destructor-and-garbage-collection-notes/) — so are not a good way to ensure garbage collection. – martineau Jun 14 '22 at 09:43
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jun 14 '22 at 11:39