Replacement for runDispatchLoopUntil()?

Regarding runDispatchLoop, the documentation says :

Also, I am a bit surprised it works to create and show the AlertWindow in your thread’s run() method. Generally, the Juce GUI objects are not designed to be used in other threads than the GUI/main thread.

Yes, I saw the note about using runDispatchLoop() on application startup. In my limited knowledge, I did not know of another way to initiate the thread. How would the thread be launched?

I moved the AlertWindow to the thread’s run() method in an attempt to simplify everything. I realize that is probably sketchy. It can easily be moved back to runTaskWithProgressBar().

I am sure this has a relatively simple solution. I have tried a number of things, but I do not have sufficient knowledge of threads to find the right code.

Maybe something like this would work:

  • Make your AlertWindow and progress into class data members, to ensure that they will live at least as long as the long-running task.
  • When job is ready to start, call enterModalState on the alert window, and set the progress to zero, all from the message thread.
  • Run the task on a background thread, querying the progress periodically. You can use MessageManager::callAsync to update the progress variable on the main thread.
  • Once the task is complete, you can use MessageManager::callAsync again to signal that the job is done, and launch a new AlertWindow.

Your main problem seems to be that you apparently want to have a stand alone function (the runTaskWithProgressBar) that would handle everything locally inside the function. But that’s not really feasible to do when following the new recommended async/non-modal model in Juce. You need to have some object that holds both the AlertWindow and the thread for the duration of the thread task. (You could maybe hack together something with heap allocations and self-destructing objects in order to run it all within a stand alone function, but that feels a bit iffy to do…)

Taking one step back I wonder why you are not using ThreadWithProgressWindow?

Although it should be noted the example shows the undesired blocking version. Instead launchThread() should be preferred.

EDIT: scrolling back I see that @xenakios suggested that already but without a response. Sorry for re-posting

I have been attempting to find a solution using ThreadWithProgressWindow. But again, my lack of knowledge is resulting in no luck.

void runTaskWithProgressBar(te::ThreadPoolJobWithProgress& t) override
{
	TaskRunner taskRunner{ t };
	taskRunner.launchThread();
}

//==============================================================================
struct TaskRunner : public ThreadWithProgressWindow
{
	TaskRunner(te::ThreadPoolJobWithProgress& t) : ThreadWithProgressWindow(t.getJobName(), true, false), task(t)
	{
		startThread();
	}

	~TaskRunner()
	{
		task.signalJobShouldExit();
		while (waitForThreadToExit(10000));
	}

	void run() override
	{
		while(!threadShouldExit())
		{
			setProgress(task.getCurrentTaskProgress());
			if (task.runJob() == ThreadPoolJob::jobHasFinished)
				break;
		}
	}
	te::ThreadPoolJobWithProgress& task;
};

BTW, this is all called from the TracktionEngine render function;

if (te::Renderer::renderToFile("Render", renderFile, *edit, range, tracksToDo, true, {}, true))
	AlertWindow::showMessageBoxAsync(AlertWindow::InfoIcon, "Rendered", renderFile.getFullPathName());
else
	AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "Render", "Failed!");

The current symptoms are;

  1. The render progress AlertWindow appears, but remains at 0%.
  2. The render does not happen.

I would expect the call to task.runJob() to make the render proceed. But it does not.

I have much to learn on all of this, so any help is most appreciated!

Well, similar to another code snippet you posted before, the main issue is that your TaskRunner is declared locally in a method that returns immediately (on purpose). In that moment the TaskRunner will be destroyed and the Task is terminated.

The TaskRunner stops the task in the destructor, that’s why you don’t see progress.

I don’t know why the window doesn’t immediately disappears though.

Try making the TaskRunner a member of the parent class.

I updated my code to the following;

void runTaskWithProgressBar(te::ThreadPoolJobWithProgress& t) override
{
	taskRunner = std::make_unique<TaskRunner>( t );
	taskRunner->launchThread();
}

//==============================================================================
struct TaskRunner : public ThreadWithProgressWindow
{
	TaskRunner(te::ThreadPoolJobWithProgress& t) : ThreadWithProgressWindow(t.getJobName(), true, false), task(t)
	{
		startThread();
	}

	~TaskRunner()
	{
		task.signalJobShouldExit();
		while (waitForThreadToExit(10000));
	}

	void run() override
	{
		while(!threadShouldExit())
		{
			setProgress(task.getCurrentTaskProgress());
			if (task.runJob() == ThreadPoolJob::jobHasFinished)
				break;
		}
	}
	te::ThreadPoolJobWithProgress& task;
};
std::unique_ptr<TaskRunner> taskRunner;

But, the AlertWindow appears briefly and then the DAW crashes!

The crash log might help.

But I realize you are using classes from the tracktion engine. I have no experience with that, e.g. te::ThreadPoolJobWithProgress.

The problem might not even be in this code snippet.

Below is the CallStack, if that is useful?

vcruntime140d.dll!memcpy_repmovs() Line 40
	at D:\agent\_work\61\s\src\vctools\crt\vcruntime\src\string\amd64\memcpy.asm(40)
[Inline Frame] TheDAW.exe!juce::ArrayBase<tracktion_engine::Clip *,juce::DummyCriticalSection>::addArrayInternal(tracktion_engine::Clip * const *) Line 406
	at C:\SDKs\JUCE\modules\juce_core\containers\juce_ArrayBase.h(406)
[Inline Frame] TheDAW.exe!juce::ArrayBase<tracktion_engine::Clip *,juce::DummyCriticalSection>::addArray(tracktion_engine::Clip * const *) Line 283
	at C:\SDKs\JUCE\modules\juce_core\containers\juce_ArrayBase.h(283)
TheDAW.exe!juce::Array<tracktion_engine::Clip *,juce::DummyCriticalSection,0>::Array<tracktion_engine::Clip *,juce::DummyCriticalSection,0>(const juce::Array<tracktion_engine::Clip *,juce::DummyCriticalSection,0> & other) Line 71
	at C:\SDKs\JUCE\modules\juce_core\containers\juce_Array.h(71)
TheDAW.exe!tracktion_engine::Renderer::Parameters::Parameters(const tracktion_engine::Renderer::Parameters & __that) Line 40
	at C:\SDKs\TracktionEngine\modules\tracktion_engine\model\export\tracktion_Renderer.h(40)
TheDAW.exe!tracktion_engine::Renderer::RenderTask::RendererContext::RendererContext(tracktion_engine::Renderer::RenderTask & owner_, tracktion_engine::Renderer::Parameters & p, tracktion_engine::AudioNode * n, juce::AudioFormatWriter::ThreadedWriter::IncomingDataReceiver * sourceToUpdate_) Line 191
	at C:\SDKs\TracktionEngine\modules\tracktion_engine\model\export\tracktion_Renderer.cpp(191)
[External Code]
TheDAW.exe!tracktion_engine::MessageThreadCallback::handleAsyncUpdate() Line 201
	at C:\SDKs\TracktionEngine\modules\tracktion_engine\utilities\tracktion_AsyncFunctionUtils.h(201)
[Inline Frame] TheDAW.exe!juce::InternalMessageQueue::dispatchMessage(juce::MessageManager::MessageBase *) Line 198
	at C:\SDKs\JUCE\modules\juce_events\native\juce_win32_Messaging.cpp(198)
TheDAW.exe!juce::InternalMessageQueue::dispatchMessages() Line 240
	at C:\SDKs\JUCE\modules\juce_events\native\juce_win32_Messaging.cpp(240)
TheDAW.exe!juce::InternalMessageQueue::dispatchNextMessage(bool returnIfNoPendingMessages) Line 124
	at C:\SDKs\JUCE\modules\juce_events\native\juce_win32_Messaging.cpp(124)
[Inline Frame] TheDAW.exe!juce::dispatchNextMessageOnSystemQueue(bool) Line 266
	at C:\SDKs\JUCE\modules\juce_events\native\juce_win32_Messaging.cpp(266)
[Inline Frame] TheDAW.exe!juce::MessageManager::runDispatchLoop() Line 107
	at C:\SDKs\JUCE\modules\juce_events\messages\juce_MessageManager.cpp(107)
[Inline Frame] TheDAW.exe!juce::JUCEApplicationBase::main() Line 262
	at C:\SDKs\JUCE\modules\juce_events\messages\juce_ApplicationBase.cpp(262)
TheDAW.exe!WinMain(HINSTANCE__ * __formal, HINSTANCE__ * __formal, char * __formal, int __formal) Line 103
	at C:\AAXProjects2\TheDAW\Source\Main.cpp(103)
[External Code]

Maybe @dave96 or @RolandMR can answer that. They know at least the tracktion part.

It might be a good idea to change the category to #tracktion-engine or create a new topic in that category.

Thank you. I will probably do that after trying a few more things.

It is frustrating because it works just fine as a modal dialog, and has been for a couple of years. But I am refactoring my code to remove modal loops and this is one of the last ones to figure out.

I appreciate everyone’s suggestions!

I imagine the ThreadPoolJobWithProgress is getting deleted when the runTaskWithProgressBar function returns?

Do you have to make this non-modal now?
I appreciate modal loops are bad and we should not be using them in new code but this codebase has relied on them for 20 years so it’s not simple to just remove them. We’ll probably need to add alternative methods that request a task to be started and provide a callback to be notified when it ends.

There might be some way to hack it in the mean time but I don’t really have the bandwidth to look in to it right now I’m afraid.

I can compile with the preprocessor value JUCE_MODAL_LOOPS_PERMITTED = 1 for the time being. I was just trying to be up-to-date.

As always, thank you for your help!

I’m also stuck with this problem for different reasons - unit testing.

TEST_CASE("Testing async calls")
{
    juce::ScopedJuceInitialiser_GUI gui;
    int x = 0;
    juce::MessageManager::callAsync([&x = x] { x = 3; });

    //This following section used to be:
    //juce::MessageManager::getInstance()->runDispatchLoopUntil(1);

    //I tried to replace it with:
    while(x == 0)
    {
        juce::MessageManager::getInstance()->runDispatchLoop();
        juce::Thread::sleep(1);
        //But, that loop never ends...
    }

    REQUIRE(x == 3);
}

Any ideas?

My understanding is that there is no simple way to achieve the functionality of juce::MessageManager::getInstance()->runDispatchLoopUntil(1);, so you must use that, along with the JUCE_MODAL_LOOPS_PERMITTED = 1 flag for the time being.

1 Like

Thank you!
Hopefully that gets fixed one day so I can also turn the modal loops off. :slight_smile:

I just ran into a similar issue with some unit tests. This pattern seems to be working for me so far:

TEST_CASE("Testing async calls")
{
    juce::ScopedJuceInitialiser_GUI gui;
    int x = 0;
    juce::MessageManager::callAsync([&x = x] { x = 3; });

    //This following section used to be:
    //juce::MessageManager::getInstance()->runDispatchLoopUntil(1);

    //Replacing it with...
    juce::Timer::callAfterDelay (1, [=]
    {
        juce::MessageManager::getInstance()->stopDispatchLoop();
    });
    juce::MessageManager::getInstance()->runDispatchLoop();

    REQUIRE(x == 3);
}
3 Likes

Just tested and the above example isn’t working for me on Mac unfortunately. The message loop isn’t actually running when calling runDispatchLoop() so in the above example x remains at 0.

Can anyone from the JUCE team reply if there’s a way to manually do that without modal loops?

Yeah same here, works on Linux, not on my Mac. It seems [NSApp run] is not starting the loop properly, with NSApp being nil. This is problematic for me as I need to run unit tests in this way. If anyone has updates let me know