ChildProcess::readProcessOutput eats up too much CPU when the child process has no output in Windows

When the child process does not have any new output, calling ChildProcess::readProcessOutput will block there with extremely high CPU utilization (occupying an entire logical core).

Observing the implementation of this method, I found that in line 451 of the ChildProcess::ActiveProcess::read method (in juce_win32_Threads.cpp), when the child process has no new output to read, the method calls Thread::yield to yield the time slot of the current CPU. But when there are not many other tasks currently being executed in the system (for example, in a server environment), doing so is no different from an empty loop without any rest. If you change this line to “Thread::sleep(1);”, the CPU load will drop instantly.

Can you see if such a solution is reasonable, or do you have a better solution?
In short, I hope this problem can be solved. The current implementation with such a high CPU usage is not acceptable.

To be clear, are you observing abnormal system behavior because of this, or are you just observing the cpu usage?

There is nothing abnormal in the system, but the CPU usage of the program using ChildProcess will be high.
But this is still a serious issue. I am writing a high-performance server program. Each concurrent request will create a child worker process. The parent process will monitor the standard output of these child worker processes in real time. I expect this monitoring action should not take up too much CPU resources. However, The fact is that when these worker processes have no output, juce::ChildProcess::readProcessOutput will spin in place with a high CPU usage…

The direct result of this is that when the line “Thread::yield();” mentioned above is changed to “Thread::sleep(1);”, due to the sharp drop in the CPU utilization of the monitoring thread, the maximum number of concurrency that the service can carry is also doubled compared to the time when this line was not changed.
Obviously, this performance bottleneck is caused by problems with the internal code of JUCE, so this should be a problem that the JUCE team should solve.
Therefore, I hope the JUCE team can solve the above-mentioned problem.

1 Like

Using Thread::sleep (1) there seems like a reasonable solution - internally it calls WaitForSingleObject() which will still yield the thread’s time slice, but unlike Thread::yield() which calls Sleep (0);, it is guaranteed to wait at least 1ms so won’t eat up all the CPU. We’ll push this to develop shortly - thanks for reporting!

1 Like

Is it safe to use the same event for all short calls to Thread::sleep? Or does that mean multiple concurrent calls will all wake on the same event? Perhaps that doesn’t matter for short sleeps?

If you look at the code for the Windows Thread::sleep() implementation, it only uses the timeout in WaitForSingleObject() - the actual object is never signaled from anywhere. Presumably because this is more accurate than Sleep() for smaller wait times as per the comment above the call:

// unlike Sleep() this is guaranteed to return to the current thread after
// the time expires, so we'll use this for short waits, which are more likely
// to need to be accurate

Ah sorry, doing too many things at once :man_facepalming: I see, it’s just using the event as a dummy and waiting for the timeout to happen. Thanks!

1 Like