Skip to content

Qt 6.9.0 hangs when stopping a program during inference. #570

Open

Description

@Mysticial

Copying my Discord post verbatim:

There's something fucked up with Qt 6.9.0 that I can't explain. If you try to stop a program while an inference is running, there's a good chance it will hang.

Reproduce steps:

  • Compile latest on Qt 6.9.0.
  • Run the Watt Farmer with no program open.
  • Wait until the program appears to get stuck trying to enter game.
  • Click "Stop Program". The program will never stop and just hangs on "Stopping program".

When I try to follow the wait chain, it doesn't make sense. The thread that it's waiting on is our thread, but the debugger is showing it as a Qt rendering thread.

When I put logs to track our thread that it was waiting on, it ends properly, but then the thread-id somehow gets (immediately) reused or reassigned to a Qt rendering thread that doesn't end. So when we try to join our thread, it's actually waiting for that Qt rendering thread forever.

It does not reproduce with an earlier build with Qt 6.8.3.

I'm going to rebuild latest with Qt 6.8.3 and see if it happens there.

Yup, no issue on Qt 6.8.3. I'll rebuild the latest release candidate with Qt 6.8.3.

I'm not satisfied with "just waiting for a newer Qt." The nature of this bug is really concerning as the thread ID gets immediately reassigned. This suggests that it is happening at the OS-level. But why does the Qt version matter? And why does it only happen with the specific thread being joined and the Qt rendering thread? The program makes a lot of other threads, yet it's only this pair that gets "merged".

This will not block v0.52.x release since I'm rolling it back to Qt 6.8.3. But it's very concerning nonetheless.

Activity

  1. Mysticial commented on Apr 25, 2025

    @Mysticial
    CollaboratorAuthor
  2. pifopi commented on Jul 25, 2025

    @pifopi
    Member

    Still broken with Qt 6.9.1

  3. Mysticial commented on Oct 12, 2025

    @Mysticial
    CollaboratorAuthor

    This appears to be fixed in Qt 6.9.3 and 6.10.0. So the Qt bug I linked is not it.

  4. Mysticial commented on Oct 20, 2025

    @Mysticial
    CollaboratorAuthor

    Nope, reproduced on Qt 6.10.0. Goddammit.

  5. Mysticial commented on Nov 7, 2025

    @Mysticial
    CollaboratorAuthor
  6. Mysticial commented on Nov 7, 2025

    @Mysticial
    CollaboratorAuthor

    This work-around is not sufficient. It just found other places to hang:

    • Inside QAudioSource::stop()
    • Inside Discord rich presence.
    • After main() exits.

    This is pretty hopeless.

  7. Mysticial commented on Feb 1, 2026

    @Mysticial
    CollaboratorAuthor

    Latest attempts to work-around this have failed:

    It seems that any of the following:

    1. Call any Qt signal/slot from an std::thread. (this was never supposed to work anyway and we no longer do this)
    2. Call QVideoFrame::toImage() from an std::thread. (is it using signals/slots internally?)
    3. Use std::condition_variable on a QThread.

    will cause at least of the following:

    1. std::thread::join() hangs because the thread gets adopted by a different Qt task.
    2. QThread::wait() hangs (unknown reason).
    3. Process hangs after returning from main(). (requiring Task Manager to kill)

    While #1 and #3 can be worked around by detaching instead of joining and exit() respectively, #2 cannot be worked around. There also seems to be global Qt state that gets corrupted upon any of the 3 triggers.

    It is not feasible to completely change everything to Qt's threading library because:

    1. Qt's threading library is shit and unnatural C++.
    2. We do not control the threads from external libraries like DPP, ONNX, etc...
    3. We are trying to minimize and isolate our Qt usage. Not increase it.

    While it's not impossible to wrap every single C++ async function with an intermediate API that is backed by either C++ or Qt, there is a lot of stuff that would need to be wrapped. std::thread, std::mutex, std::condition_variable. And it would ruin any sort of idiomatic C++ async coding which we prefer.

  8. added
    existential-threatBugs that, if not fixed, may become an existential threat to the project in the long run.
    help wantedExtra attention is needed
    on Feb 1, 2026
  9. Mysticial commented on Feb 14, 2026

    @Mysticial
    CollaboratorAuthor

    Pulling from here: https://discord.com/channels/695809740428673034/1462210406616531259/1471708678502613022

    I'm really hoping it works now - as ugly as it is. But seeing as how I've been wrong about this about 10 times now, I'm probably wrong again. lol

    As for why the workaround works, we'll need to start with the problem. The problem is that in Qt 6.9+, when a thread ends, it can (but not always) get adopted by another thread handle. This seems to be some sort of attempt at thread pooling on Qt's part.

    The bug seems to be in that adoption logic where a thread handle who's thread has ended will randomly adopt a non-Qt thread.

    So if thread A joins its own thread, after the thread ends, the handle (incorrectly) adopts thread B. So thread.join() begins waiting for thread B to finish. But thread B is unrelated and thus it may never return.

    Initially I only observed this issue when mixing std::thread and QThread - the former lacking the initialization to properly enter Qt's centralized thread management. But after I refactored the entire codebase to wrap threads (and their associated mutexes and condition variables) and pointed it all to Qt's versions (thus no more mixing), I was still seeing hangs in QThread's own join(). And it turns out that even though I made all of our threads into "compliance", Qt's video stack itself calls into AMD's drivers which spawn a ton of threads outside of Qt. These video rendering threads (which are not Qt threads) are not in compliance, and end up getting adoped by QThread handles causing them to hang on join.

    The issue only seems to happen when a thread ends since only when a thread ends can it trigger the adoption mechanism. So the solution is to simply never let any thread end. Hence the full refactor of the entire codebase to use only thread pools.

    But this wasn't quite enough. When a program exits, it destructs everything (even if you call exit(0)). And it's illegal to destruct a QThread without joining its thread. (whereas std::thread has a detach() function). Forcing QThread's to end doesn't work since it hangs in the destructor. So the final solution was to simply leak all QThreads so they don't try to join their threads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions