Exiting a thread vs. putting it to sleep

My program does a lot of heavy text parsing, so I’m using juce::Thread to get this task off of the message thread. The Thread::run() method runs in a loop, looking for new text to parse on each iteration, like this:

void run() override
{
    while (!threadShouldExit())
    {
        parseTextIfNecessary();

        if (threadShouldExit())
            return;

        wait(1);
    }
}

This works well, but it also spends a lot of time doing nothing when there’s no text to parse, so I’m hoping to improve it. I think it would be a lot more efficient if I could suspend the loop once all of the text is parsed, and then re-activate it when the text has changed. I can see two options for this: exiting the the thread, or putting it to sleep.

The first option would look something like this:

void run() override
{
    while (!threadShouldExit())
    {
        bool somethingLeftToParseNextTime = parseTextIfNecessary();

        if (somethingLeftToParseNextTime)
            signalThreadShouldExit();

        if (threadShouldExit())
            return;

        wait(1);
    }
}

void textHasBeenUpdated()  // this is called to indicate that the text has been updated and needs to be parsed
{
    startThread();
}

The second option would look like this:

void run() override
{
    while (!threadShouldExit())
    {
        bool somethingLeftToParseNextTime = parseTextIfNecessary();

        if (threadShouldExit())
            return;

        wait(somethingLeftToParseNextTime ? 1 : -1);
    }
}

void textHasBeenUpdated()
{
    notify();
}

My question is: what are the relative advantages and disadvantages of each method, and is one clearly better than the other in my case? Alternatively, is there another method that I should be thinking about?

IMHO I never use juce::Thread directly. Juce has methods to create reusable multi-purpose threads that just lie around waiting for your job:

juce::ThreadPool is perfect for background jobs that have a clear end. In your use case you could simply create a new juce::ThreadPoolJob whenever new text arrives.
AFAIK those jobs are always run from start to end without interruption.

juce::TimeSliceThread is great for tasks that sit in the background and wait for work. To see how it is used you can have a look into juce::BufferingAudioSource:

The useTimeSlice is run regularly and you can specify how long to wait before it is called again (all clients sharing the same TimeSliceThread have a common queue, so the next call will be scheduled amongst the clients.

The only drawbacks I found is with ThreadPoolJobs (I use them to update FilmStrips in my video editor) if you want to restart it needs a little thought how to finish the running one gracefully and restart with the new settings. But that problem you would have anyway.

1 Like

Thanks for responding Daniel. I had looked into ThreadPool but thought it might be more complicated than necessary. I’m not familiar with TimeSliceThread, but I don’t yet see what advantage it would have. Could you explain a bit more about why you avoid using Thread directly?

The way my program is designed, I only ever have one parsing thread which is doing all the work.

Maybe in your case it doesn’t matter so much, if it’s ever going to be that one thread.

My reasons:

  • saves the overhead of starting and stopping the thread
  • you can configure easily at runtime to match the machine you’re running on:
    Ideally you have as many threads as cores. When I create a ThreadPool i simply create as many threads in it as I have cores available, that way I can be certain not to flood the system with more threads than what’s healthy
  • the interface is pretty similar, so you can simply inherit from TimeSliceClient or ThreadPoolJob and adapt the run() to runJob() or useTimeSlice(), so IMHO it is not harder than creating a Thread
  • In one app I even used it to switch multi thread on and off on the fly. For multi-threading I just need either add a TimeSliceClient to a TimeSliceThread or I call the useTimeSlice() synchronously in case of single thread. That is impossible when you inherit juce::Thread, because it is already running.

But it all depends on the exact use case, so there is never just one answer.

You could use a std::condition_variable or a juce::WaitableEvent to suspend the parsing thread and wake it later, once there’s more work waiting. The parsing thread would call WaitableEvent::wait at the top of the loop, and some other thread would call WaitableEvent::signal whenever there is new input ready to process.

Thanks @reuk, this is along the lines of what I was thinking. Just to be clear, is there a difference between using WaitableEvent and calling Thread::wait()? I looked inside this function and it seems that it is calling a WaitableEvent internally, so I was thinking that I might get away without having to use WaitableEvent directly.

Also, what is the difference between wait() and sleep()?

sleep will sleep the thread unconditionally, whereas wait will allow the thread to be resumed before the timeout has elapsed in response to a call to notify.

It sounds like using wait and notify is probably the simplest way of achieving the desired effect.

2 Likes