Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix _QThreadWorker.run not releasing references to fulfilled command object before blocking on next queue.get call #113

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

kormax
Copy link

@kormax kormax commented Mar 8, 2024

When writing tests, I've noticed that when using qasync.QThreadExecutor, the objects, be it instance method owners, or something passed in as args, were not deleted immediately as the executor finished running the submitted task, even if there were no more references to those objects on the user side. The only way to release them was either to submit a new task, or shutdown an executor.

While investigating the code, I've also taken a look into concurrent.futures.ThreadPoolExecutor, which didn't exhibit this behaviour.

That's where I found the following code:

work_item = work_queue.get(block=True)
if work_item is not None:
    work_item.run()
    # Delete references to object. See issue16284
    del work_item

This PR mimics this change by adding a manual deletion of references to command tuple and its inner parameters inside of _QThreadWorker.run;

del command, future, callback, args, kwargs

The change can be tested by running the following example:

from qasync import QThreadExecutor
from concurrent.futures import ThreadPoolExecutor


class Demo:
    def __init__(self):
        print(f"Py ini {self}")

    def __del__(self):
        print(f"Py del {self}")

    def example(self):
        return


if __name__ == "__main__":
    threadpool_demo_obj = Demo()

    executor = ThreadPoolExecutor()
    future = executor.submit(threadpool_demo_obj.example)
    del threadpool_demo_obj
    future.result()
    print(f"Shutting down threadpool executor")
    executor.shutdown()

    qthreadpool_demo_obj = Demo()
    executor = QThreadExecutor()
    future = executor.submit(qthreadpool_demo_obj.example)
    del qthreadpool_demo_obj
    future.result()
    print(f"Shutting down qthreadpool executor")
    executor.shutdown()

Before:

Py ini <__main__.Demo object at 0x103014250>
Py del <__main__.Demo object at 0x103014250>
Shutting down threadpool executor
Py ini <__main__.Demo object at 0x103014250>
Shutting down qthreadpool executor
Py del <__main__.Demo object at 0x103014250>

After:

Py ini <__main__.Demo object at 0x104f68250>
Py del <__main__.Demo object at 0x104f68250>
Shutting down threadpool executor
Py ini <__main__.Demo object at 0x104f68250>
Py del <__main__.Demo object at 0x104f68250>
Shutting down qthreadpool executor

As for potential downsides of this change: there could theoretically be code that accidentally relies on QThreadExecutor not releasing references, especially if QObject, Slots and other Qt-related stuff is involved.
But even if it's a big issue, it still might be worth adding an option to enable proper behaviour via an environment variable or other parameter.

…object before blocking on next queue.get call
@kormax
Copy link
Author

kormax commented Mar 8, 2024

Thinking about it, the r variable should also be set to None at the end of related block to prevent the same thing happening to the result object.

@kormax
Copy link
Author

kormax commented Jul 6, 2024

Hi @hosaka ,

hope you are well.

Sorry for a direct mention, but you were the last person ever active in this repository.

May I ask you if it is possible to get this PR reviewed or approved and merged?

Regards,

  • Max

@hosaka
Copy link
Collaborator

hosaka commented Jul 8, 2024

Hello @kormax ,

I am a bit concerned about an effect this will have on QObjects, because the ownership/lifetime is managed internally in PySide at least (Shiboken). You do mention it as a "potential downside", I think this could be an issue for code that uses QObjects and Slots and most of the code using this library probably does use QObjects.

At least some tests should be performed to see if this might cause a double free.

@hosaka hosaka added the enhancement New feature or request label Jul 8, 2024
@hosaka
Copy link
Collaborator

hosaka commented Jul 8, 2024

After a bit of testing with QObjects and a codebase of mine that relies on qasync I can see no issues related to this change, including parent-child objects. Just like the comments in cpython issue state, the worker should not keep these references.

@kormax it would be nice to have a new test in test_qthreadexec.py, akin to https://github.com/python/cpython/blob/main/Lib/test/test_concurrent_futures/executor.py#L80, is it something that you could add to this change?

@hosaka hosaka assigned hosaka and unassigned hosaka Jul 8, 2024
@kormax
Copy link
Author

kormax commented Jul 8, 2024

NP. Gonna add this change a bit later .
(I've tried an hour ago, but for some reason when running example through pytest, something weird happens to rc/gc and test results are inconclusive, while entirely stable in normal code).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants