Browse Source

Thread: Fix realtime threads on macOS

- macOS behaviour of setRealtime now matches other platforms

MR feedback
v7.0.9
Anthony Nicholls 2 years ago
parent
commit
407720b557
5 changed files with 212 additions and 42 deletions
  1. +23
    -0
      BREAKING-CHANGES.txt
  2. +2
    -2
      modules/juce_core/native/juce_SharedCode_posix.h
  3. +78
    -31
      modules/juce_core/native/juce_Threads_mac.mm
  4. +2
    -2
      modules/juce_core/threads/juce_Thread.cpp
  5. +107
    -7
      modules/juce_core/threads/juce_Thread.h

+ 23
- 0
BREAKING-CHANGES.txt View File

@@ -4,6 +4,29 @@ JUCE breaking changes
develop
=======

Change
------
RealtimeOptions member workDurationMs was replaced by three optional member
variables in RealtimeOptions, and all RealtimeOptions member variables were
marked private.

Possible Issues
---------------
Trying to construct a RealtimeOptions object with one or two values, or access
any of its member variables, will no longer compile.

Workaround
----------
Use the withMember functions to construct the object, and the getter functions
to access the member variable values.

Rationale
---------
The new approach improves the flexibility for users to specify realtime thread
options on macOS/iOS and improves the flexibility for the API to evolve without
introducing further breaking changes.


Change
------
JUCE module compilation files with a platform suffix are now checked case


+ 2
- 2
modules/juce_core/native/juce_SharedCode_posix.h View File

@@ -894,7 +894,7 @@ public:
const auto min = jmax (0, sched_get_priority_min (SCHED_RR));
const auto max = jmax (1, sched_get_priority_max (SCHED_RR));
return jmap (rt->priority, 0, 10, min, max);
return jmap (rt->getPriority(), 0, 10, min, max);
}
// We only use this helper if we're on an old macos/ios platform that might
@@ -959,7 +959,7 @@ private:
int priority;
};
static void* makeThreadHandle (PosixThreadAttribute& attr, Thread* userData, void* (*threadEntryProc) (void*))
static void* makeThreadHandle (PosixThreadAttribute& attr, void* userData, void* (*threadEntryProc) (void*))
{
pthread_t handle = {};


+ 78
- 31
modules/juce_core/native/juce_Threads_mac.mm View File

@@ -64,28 +64,99 @@ static auto getJucePriority (qos_class_t qos)
return Thread::Priority::normal;
}
template<typename Type>
static std::optional<Type> firstOptionalWithValue (const std::initializer_list<std::optional<Type>>& optionals)
{
for (const auto& optional : optionals)
if (optional.has_value())
return optional;
return {};
}
static bool tryToUpgradeCurrentThreadToRealtime (const Thread::RealtimeOptions& options)
{
const auto periodMs = options.getPeriodMs().value_or (0.0);
const auto processingTimeMs = firstOptionalWithValue (
{
options.getProcessingTimeMs(),
options.getMaximumProcessingTimeMs(),
options.getPeriodMs()
}).value_or (10.0);
const auto maxProcessingTimeMs = options.getMaximumProcessingTimeMs()
.value_or (processingTimeMs);
// The processing time can not exceed the maximum processing time!
jassert (maxProcessingTimeMs >= processingTimeMs);
thread_time_constraint_policy_data_t policy;
policy.period = (uint32_t) Time::secondsToHighResolutionTicks (periodMs / 1'000.0);
policy.computation = (uint32_t) Time::secondsToHighResolutionTicks (processingTimeMs / 1'000.0);
policy.constraint = (uint32_t) Time::secondsToHighResolutionTicks (maxProcessingTimeMs / 1'000.0);
policy.preemptible = true;
const auto result = thread_policy_set (pthread_mach_thread_np (pthread_self()),
THREAD_TIME_CONSTRAINT_POLICY,
(thread_policy_t) &policy,
THREAD_TIME_CONSTRAINT_POLICY_COUNT);
if (result == KERN_SUCCESS)
return true;
// testing has shown that passing a computation value > 50ms can
// lead to thread_policy_set returning an error indicating that an
// invalid argument was passed. If that happens this code tries to
// limit that value in the hope of resolving the issue.
if (result == KERN_INVALID_ARGUMENT && options.getProcessingTimeMs() > 50.0)
return tryToUpgradeCurrentThreadToRealtime (options.withProcessingTimeMs (50.0));
return false;
}
bool Thread::createNativeThread (Priority priority)
{
PosixThreadAttribute attr { threadStackSize };
PosixThreadAttribute attribute { threadStackSize };
if (@available (macos 10.10, *))
pthread_attr_set_qos_class_np (attr.get(), getNativeQOS (priority), 0);
pthread_attr_set_qos_class_np (attribute.get(), getNativeQOS (priority), 0);
else
PosixSchedulerPriority::getNativeSchedulerAndPriority (realtimeOptions, priority).apply (attr);
PosixSchedulerPriority::getNativeSchedulerAndPriority (realtimeOptions, priority).apply (attribute);
struct ThreadData
{
Thread& thread;
std::promise<bool> started;
};
ThreadData threadData { *this };
threadId = threadHandle = makeThreadHandle (attr, this, [] (void* userData) -> void*
threadId = threadHandle = makeThreadHandle (attribute, &threadData, [] (void* userData) -> void*
{
auto* myself = static_cast<Thread*> (userData);
auto& data { *static_cast<ThreadData*> (userData) };
auto& thread = data.thread;
if (thread.isRealtime()
&& ! tryToUpgradeCurrentThreadToRealtime (*thread.realtimeOptions))
{
data.started.set_value (false);
return nullptr;
}
data.started.set_value (true);
JUCE_AUTORELEASEPOOL
{
juce_threadEntryPoint (myself);
juce_threadEntryPoint (&thread);
}
return nullptr;
});
return threadId != nullptr;
return threadId != nullptr
&& threadData.started.get_future().get();
}
void Thread::killThread()
@@ -122,30 +193,6 @@ bool Thread::setPriority (Priority priority)
{
jassert (Thread::getCurrentThreadId() == getThreadId());
if (isRealtime())
{
// macOS/iOS needs to know how much time you need!
jassert (realtimeOptions->workDurationMs > 0);
mach_timebase_info_data_t timebase;
mach_timebase_info (&timebase);
const auto periodMs = realtimeOptions->workDurationMs;
const auto ticksPerMs = ((double) timebase.denom * 1000000.0) / (double) timebase.numer;
const auto periodTicks = (uint32_t) jmin ((double) std::numeric_limits<uint32_t>::max(), periodMs * ticksPerMs);
thread_time_constraint_policy_data_t policy;
policy.period = periodTicks;
policy.computation = jmin ((uint32_t) 50000, policy.period);
policy.constraint = policy.period;
policy.preemptible = true;
return thread_policy_set (pthread_mach_thread_np (pthread_self()),
THREAD_TIME_CONSTRAINT_POLICY,
(thread_policy_t) &policy,
THREAD_TIME_CONSTRAINT_POLICY_COUNT) == KERN_SUCCESS;
}
if (@available (macOS 10.10, *))
return pthread_set_qos_class_self_np (getNativeQOS (priority), 0) == 0;


+ 2
- 2
modules/juce_core/threads/juce_Thread.cpp View File

@@ -166,7 +166,7 @@ bool Thread::startRealtimeThread (const RealtimeOptions& options)
if (threadHandle == nullptr)
{
realtimeOptions = makeOptional (options);
realtimeOptions = std::make_optional (options);
if (startThreadInternal (Priority::normal))
return true;
@@ -276,7 +276,7 @@ void Thread::removeListener (Listener* listener)
bool Thread::isRealtime() const
{
return realtimeOptions.hasValue();
return realtimeOptions.has_value();
}
void Thread::setAffinityMask (const uint32 newAffinityMask)


+ 107
- 7
modules/juce_core/threads/juce_Thread.h View File

@@ -75,14 +75,114 @@ public:
*/
struct RealtimeOptions
{
/** Linux only: A value with a range of 0-10, where 10 is the highest priority. */
int priority = 5;
/** A value with a range of 0-10, where 10 is the highest priority.
/** iOS/macOS only: A millisecond value representing the estimated time between each
'Thread::run' call. Your thread may be penalised if you frequently
overrun this.
Currently only used by Posix platforms.
@see getPriority
*/
[[nodiscard]] RealtimeOptions withPriority (int newPriority) const
{
jassert (isPositiveAndNotGreaterThan (newPriority, 10));
return withMember (*this, &RealtimeOptions::priority, juce::jlimit (newPriority, 0, 10));
}
/** Specify the expected amount of processing time required each time the thread wakes up.
Only used by macOS/iOS.
@see getProcessingTimeMs, withMaximumProcessingTimeMs, withPeriodMs, withPeriodHz
*/
[[nodiscard]] RealtimeOptions withProcessingTimeMs (double newProcessingTimeMs) const
{
jassert (newProcessingTimeMs > 0.0);
return withMember (*this, &RealtimeOptions::processingTimeMs, newProcessingTimeMs);
}
/** Specify the maximum amount of processing time required each time the thread wakes up.
Only used by macOS/iOS.
@see getMaximumProcessingTimeMs, withProcessingTimeMs, withPeriodMs, withPeriodHz
*/
[[nodiscard]] RealtimeOptions withMaximumProcessingTimeMs (double newMaximumProcessingTimeMs) const
{
jassert (newMaximumProcessingTimeMs > 0.0);
return withMember (*this, &RealtimeOptions::maximumProcessingTimeMs, newMaximumProcessingTimeMs);
}
/** Specify the approximate amount of time between each thread wake up.
Alternatively call withPeriodHz().
Only used by macOS/iOS.
@see getPeriodMs, withPeriodHz, withProcessingTimeMs, withMaximumProcessingTimeMs,
*/
[[nodiscard]] RealtimeOptions withPeriodMs (double newPeriodMs) const
{
jassert (newPeriodMs > 0.0);
return withMember (*this, &RealtimeOptions::periodMs, newPeriodMs);
}
/** Specify the approximate frequency at which the thread will be woken up.
Alternatively call withPeriodMs().
Only used by macOS/iOS.
@see getPeriodHz, withPeriodMs, withProcessingTimeMs, withMaximumProcessingTimeMs,
*/
[[nodiscard]] RealtimeOptions withPeriodHz (double newPeriodHz) const
{
jassert (newPeriodHz > 0.0);
return withPeriodMs (1'000.0 / newPeriodHz);
}
/** Returns a value with a range of 0-10, where 10 is the highest priority.
@see withPriority
*/
uint32_t workDurationMs = 0;
[[nodiscard]] int getPriority() const
{
return priority;
}
/** Returns the expected amount of processing time required each time the thread
wakes up.
@see withProcessingTimeMs, getMaximumProcessingTimeMs, getPeriodMs
*/
[[nodiscard]] std::optional<double> getProcessingTimeMs() const
{
return processingTimeMs;
}
/** Returns the maximum amount of processing time required each time the thread
wakes up.
@see withMaximumProcessingTimeMs, getProcessingTimeMs, getPeriodMs
*/
[[nodiscard]] std::optional<double> getMaximumProcessingTimeMs() const
{
return maximumProcessingTimeMs;
}
/** Returns the approximate amount of time between each thread wake up, or
nullopt if there is no inherent periodicity.
@see withPeriodMs, withPeriodHz, getProcessingTimeMs, getMaximumProcessingTimeMs
*/
[[nodiscard]] std::optional<double> getPeriodMs() const
{
return periodMs;
}
private:
int priority { 5 };
std::optional<double> processingTimeMs;
std::optional<double> maximumProcessingTimeMs;
std::optional<double> periodMs{};
};
//==============================================================================
@@ -464,7 +564,7 @@ private:
const String threadName;
std::atomic<void*> threadHandle { nullptr };
std::atomic<ThreadID> threadId { nullptr };
Optional<RealtimeOptions> realtimeOptions = {};
std::optional<RealtimeOptions> realtimeOptions = {};
CriticalSection startStopLock;
WaitableEvent startSuspensionEvent, defaultEvent;
size_t threadStackSize;


Loading…
Cancel
Save