Std::future::get/wait equivalent in juce::Thread

Hello,

Is it possible to repeat this with juce::Thread? I know about Thread::launch, but I need to wait for the thread to finish.

auto future = std::async(std::launch::async, [] { });
future.get();

I see no need for juce::Thread with futures.

In my plugins I have code like this, for instance for applying some processing on channels and implementing a simple waiting barrier:

for (size_t ch = 0; ch < numChannels; ++ch)
    futures[ch] = std::async (std::launch::async [&] { ...

for (size_t ch = 0; ch < numChannels; ++ch)
    futures[ch].wait ();

with

std::vector <std::future <void>> futures;

1 Like

Yes, I also use std::async, but I sometimes have short thread hangs. The program has a strict execution time limit. So I want to compare the behavior with juce::Thread. I also checked OpenMP. There are no such hangups. Probably, this is due to different thread priorities. All extraneous streams easily slow down std::async.

Rather than launching and destroying threads which is often expensive and timely have you considered something like the ThreadPool class. After adding a job you can call waitForJobToFinish().

1 Like

Keep in mind that waiting for a job can cause priority inversion for the thread doing the waiting. You may want to take advantage of the priority argument when constructing the ThreadPool if you need it to run fast too.

In my particular example/use case, the processing time for several channels came close to handling a single channel (talking about a time range of a few msecs - non RT audio work), which makes it work well for me.

I initially thought the original question was whether std::async / futures could be used in combination with Juce threads, sorry if I got that wrong.

Thanks!
It looks like this is what I need. I would like to find more examples. The waitForJobToFinish function requires some kind of ThreadPoolJob.

Okay, I guess myself. It should be done like this:

    juce::ThreadPool threadPool;
    threadPool.addJob([this] { infoLabel.setText ("ABC", dontSendNotification); });
    threadPool.waitForJobToFinish(threadPool.getJob(0), 100);

Maybe I’m doing something wrong, because it slows down a lot. While I will use std::async/future.

I compared ThreadPool and std::async. In these programs, in both cases, the inscription “ABC” should have appeared, but in the ThreadPool the inscription “ACB” appears. Why?

ThreadPool:

#define DELAY 2000
        addAndMakeVisible (button);
        button.onClick = [this]
        {
            infoLabel.setText ("", dontSendNotification);

            juce::ThreadPool threadPool;
            threadPool.addJob([this]
            {
                juce::Thread::sleep(DELAY - 500);
                infoLabel.setText (infoLabel.getText() + "A", dontSendNotification);
            });
            threadPool.addJob([this]
            {
                juce::Thread::sleep(DELAY);
                infoLabel.setText (infoLabel.getText() + "B", dontSendNotification);
            });
            threadPool.waitForJobToFinish(threadPool.getJob(0), 10000);
            threadPool.waitForJobToFinish(threadPool.getJob(1), 10000);
            infoLabel.setText (infoLabel.getText() + "C", dontSendNotification);
        };

std::async/future:

#define DELAY 2000
        addAndMakeVisible (button);
        button.onClick = [this]
        {
            infoLabel.setText ("", dontSendNotification);

            std::future<void> future1 = std::async([this]
            {
                juce::Thread::sleep(DELAY - 500);
                infoLabel.setText (infoLabel.getText() + "A", dontSendNotification);
            });
            std::future<void> future2 = std::async([this]
            {
                juce::Thread::sleep(DELAY);
                infoLabel.setText (infoLabel.getText() + "B", dontSendNotification);
            });
            future1.wait();
            future2.wait();
            infoLabel.setText (infoLabel.getText() + "C", dontSendNotification);
        };

Why would you want to use threads for user interface functionality?

Juce has a lot of messaging and notification support for that.

Threads imho should only be considered for areas that involve serious processing requirements.

1 Like

This is just an example. I am using streams in an audio process (processBlock).

I too was a bit thrown off by that, since anyway trying to do UI stuff from anything other than the Message Thread usually results in having a bad time, but, I presume this is just a toy example just to try and understand the execution order of threads.

It might be a better test to instead just do some modifications to a string member of the class and then DBG output it at the end of the test. (edit: should probably use some Mutex too to guard the threads trying to race against eachother despite the sleeps)

I like the ability to set the priority in the ThreadPool, but it does not work correctly.

Toy or not, it is undefined behaviour to call setText on a label from a background thread.
The textValue is accessed by the message thread without any atomic. The behaviour is somewhat random.

If you want to rule that out, prefer DBG().

The whole pool has a certain number of threads. It is sheer luck when a thread becomes available and how fast it is, if it shares the core with something demanding.
This architecture is not appropriate if you need a certain order for the threads to finish.

And note that your example blocks the message thread. If you want to update the label in the meantime, you need to pump the message queue manually, which is a terrible design.

EDIT: The bug is somewhere else:
When the first job finishes, it won’t wait for getJob(1), because that is now getJob(0) :slight_smile:

2 Likes

Thanks!
Yes, if you do this, then everything works correctly:

    threadPool.waitForJobToFinish(threadPool.getJob(0), 10000);
    threadPool.waitForJobToFinish(threadPool.getJob(0), 10000);

In the code you supplied in the std::async version the output could be “ABC” or “BAC”, in the ThreadPool version I’m not sure any guarantees apply. To be clear the delays you’ve added just change the chance that things might happen out of order, they don’t offer any guarantees.

If you want the first two to happen in order then you’ll definitely be better off without any threading. You’ll want to run things in parallel fine but be warned if we’re talking DSP in most cases in a plugin you’ll probably find it better to remain single threaded.

One option adjusting your code just slightly would be (untested)…

#define DELAY 2000
        addAndMakeVisible (button);
        button.onClick = [this]
        {
            infoLabel.setText ("", dontSendNotification);

            juce::ThreadPool threadPool;
            threadPool.addJob([this]
            {
                juce::Thread::sleep(DELAY - 500);
                infoLabel.setText (infoLabel.getText() + "A", dontSendNotification);
            });
            threadPool.addJob([this]
            {
                juce::Thread::sleep(DELAY);
                infoLabel.setText (infoLabel.getText() + "B", dontSendNotification);
            });
            while (threadPool.getNumJobs() > 0)
                threadPool.waitForJobToFinish(threadPool.getJob(0), 10000);
            infoLabel.setText (infoLabel.getText() + "C", dontSendNotification);
        };

Alternative option (untested)…

struct LambdaThreadPoolJob : ThreadPoolJob
{
    LambdaThreadPoolJob (std::function<void()> j)
        : ThreadPoolJob { "LambdaThreadPoolJob" },
          job { std::move (j) }
    {
    }

    ThreadPoolJob::JobStatus runJob() override
    {
        job();
        return ThreadPoolJob::JobStatus:: jobHasFinished;
    }

    std::function<void()> job;

    static std::unique_ptr<LambdaThreadPoolJob> create (std::function<void()> j)
    {
        return std::make_unique<LambdaThreadPoolJob> (invokable);
    }
}
auto job1 = LambdaThreadPoolJob::create ([]
{ 
    DBG ("A");
});

auto job2 = LambdaThreadPoolJob::create ([]
{ 
    DBG ("B");
});

threadPool.addJob (job1.get(), false);
threadPool.addJob (job2.get(), false);
threadPool.waitForJobToFinish (job1.get());
threadPool.waitForJobToFinish (job2.get());
1 Like

I understand. It works. Thank you!

1 Like

I checked ThreadPool. This is more stable than std::async. The main thing is not to declare the ThreadPool in the processBlock function, otherwise there will be problems.

Correct! To make this absolutely clear in case this fell over: you are using the ThreadPool to not start new threads every time you want to do stuff, but create it once during startup (ctor of your processor, maybe with std::make_unique if you want) and use the same instance every time you want to schedule stuff.
Make sure to shut it down properly in your dtor.