From 9faa2e192273670aaf60974df74a6437a017abeb Mon Sep 17 00:00:00 2001 From: CmdrShepardsPie <22330942+CmdrShepardsPie@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:06:01 +0000 Subject: [PATCH] Implemented wake-up of sleeping ipc::Thread on cancel() ipc::Thread::sleep() split the sleep of an ipc::Thread into intervals of at most 100 ms to poll the cancellation flag, so a thread that sleeps for a long time woke up at least 10 times per second, and cancel() took effect only at the end of the current interval. Added a mutex and a condition variable to ipc::Thread. cancel() now sets the cancellation flag and signals the condition variable under the lock, and sleep() waits on the condition variable for the whole interval: * POSIX: until a CLOCK_MONOTONIC deadline, or a CLOCK_REALTIME one if pthread_condattr_setclock() fails; the deadline is limited to the maximum value of time_t, so a very long sleep() lasts until cancel(). * MacOS: pthread_condattr_setclock() is not available, so sleep() waits with pthread_cond_timedwait_relative_np() for the time left until a CLOCK_MONOTONIC deadline, recomputed after each wake-up. * Windows: the time left is computed with QueryPerformanceCounter() after each return of SleepConditionVariableCS(), so neither an early timeout nor a spurious wake-up ends sleep() early. sleep() returns STATUS_CANCELLED as soon as the thread is cancelled. sleep() of a thread that is not an ipc::Thread is unchanged. The thread handle, the mutex and the condition variable are kept in the private structure detail::thread_impl_t, defined in Thread.cpp and allocated by the constructor, so ipc/Thread.h no longer includes Windows or pthread headers. The thread launchers became static members of that structure. start() returns STATUS_NO_MEM if the allocation failed. Declared a private copy constructor of ipc::Thread, as the thread now owns the platform-specific state. Added the runtime.ipc.thread_sleep unit test. Co-Authored-By: Claude --- CHANGELOG | 5 +- include/lsp-plug.in/ipc/Thread.h | 32 ++-- src/main/ipc/Thread.cpp | 257 +++++++++++++++++++++++----- src/test/utest/ipc/thread_sleep.cpp | 210 +++++++++++++++++++++++ 4 files changed, 437 insertions(+), 67 deletions(-) create mode 100644 src/test/utest/ipc/thread_sleep.cpp diff --git a/CHANGELOG b/CHANGELOG index 00c0191..4312d54 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,10 @@ ******************************************************************************* === 1.0.37 === - +* Implemented immediate wake-up of a thread sleeping in ipc::Thread::sleep() + on ipc::Thread::cancel(); the sleep is no longer split into 100 ms intervals. +* The ipc/Thread.h header no longer includes platform headers (Windows or + pthread); the platform-specific state of ipc::Thread is kept in Thread.cpp. === 1.0.36 === * Updated build scripts. diff --git a/include/lsp-plug.in/ipc/Thread.h b/include/lsp-plug.in/ipc/Thread.h index 8a75a14..af6ff96 100644 --- a/include/lsp-plug.in/ipc/Thread.h +++ b/include/lsp-plug.in/ipc/Thread.h @@ -1,6 +1,6 @@ /* - * Copyright (C) 2025 Linux Studio Plugins Project - * (C) 2025 Vladimir Sadovnikov + * Copyright (C) 2026 Linux Studio Plugins Project + * (C) 2026 Vladimir Sadovnikov * * This file is part of lsp-runtime-lib * Created on: 25 февр. 2019 г. @@ -27,12 +27,6 @@ #include #include -#if defined(PLATFORM_WINDOWS) - #include -#else - #include -#endif /* PLATFORM_WINDOWS */ - #include #include @@ -59,6 +53,11 @@ namespace lsp constexpr thread_id_t INVALID_THREAD_ID = 0; + namespace detail + { + struct thread_impl_t; + } /* namespace detail */ + /** * Thread class */ @@ -81,31 +80,26 @@ namespace lsp volatile bool bCancelled; volatile status_t nResult; -#if defined(PLATFORM_WINDOWS) - HANDLE hThread; // Windows threads -#else - pthread_t hThread; // POSIX threads -#endif /* PLATFORM_WINDOWS */ + detail::thread_impl_t *pImpl; // Platform-specific thread handle and cancellation wake-up protected: binding_t sBinding; private: -#if defined(PLATFORM_WINDOWS) - static DWORD WINAPI thread_launcher(_In_ LPVOID lpParameter); -#else - static void *thread_launcher(void *arg); -#endif /* PLATFORM_WINDOWS */ + friend struct detail::thread_impl_t; + Thread(const Thread &src); // Deny copying Thread & operator = (const Thread &src); // Deny copying + void init_impl(); + public: explicit Thread(); explicit Thread(thread_proc_t proc); explicit Thread(thread_proc_t proc, void *arg); explicit Thread(IRunnable *runnable); - virtual ~Thread(); + virtual ~Thread(); // The thread must be joined before it is destroyed public: /** diff --git a/src/main/ipc/Thread.cpp b/src/main/ipc/Thread.cpp index d00bb59..59187d2 100644 --- a/src/main/ipc/Thread.cpp +++ b/src/main/ipc/Thread.cpp @@ -30,6 +30,13 @@ #if defined(PLATFORM_WINDOWS) #include #include + #include +#else + #include +#endif /* PLATFORM_WINDOWS */ + +#if defined(PLATFORM_WINDOWS) + // Nothing #elif defined(PLATFORM_LINUX) #include #include @@ -56,20 +63,78 @@ namespace lsp { namespace ipc { + namespace detail + { + struct thread_impl_t + { + #if defined(PLATFORM_WINDOWS) + HANDLE hThread; // Windows threads + CRITICAL_SECTION sLock; // Lock for cancellation wake-up + CONDITION_VARIABLE sCancel; // Cancellation wake-up for sleep() + + static DWORD WINAPI launcher(_In_ LPVOID lpParameter); + #else + pthread_t hThread; // POSIX threads + pthread_mutex_t sLock; // Lock for cancellation wake-up + pthread_cond_t sCancel; // Cancellation wake-up for sleep() + clockid_t nClockId; // Clock used by sleep() + + static void *launcher(void *arg); + #endif /* PLATFORM_WINDOWS */ + }; + } /* namespace detail */ + __thread Thread *Thread::pThis = NULL; - - #if defined(PLATFORM_WINDOWS) - #define CLR_HANDLE(hThread) hThread = INVALID_HANDLE_VALUE; - #else - #define CLR_HANDLE(hThread) hThread = 0; - #endif + + #if !defined(PLATFORM_WINDOWS) + static void add_millis(struct timespec *ts, wsize_t millis) + { + const time_t max_sec = time_t((~uint64_t(0)) >> (65 - sizeof(time_t) * 8)); // Maximum value of time_t + const uint64_t sec = millis / 1000; + ts->tv_nsec += (millis % 1000) * 1000000; + ts->tv_sec += ts->tv_nsec / 1000000000; + ts->tv_nsec %= 1000000000; + + // Wait until cancelled if the deadline does not fit into time_t + if (sec > uint64_t(max_sec - ts->tv_sec)) + { + ts->tv_sec = max_sec; + ts->tv_nsec = 999999999; + } + else + ts->tv_sec += sec; + } + #endif /* PLATFORM_WINDOWS */ + + #if defined(PLATFORM_MACOSX) + // Convert the current time to the time left until the deadline, false if it has been reached + static bool time_left(struct timespec *left, const struct timespec *deadline) + { + if ((left->tv_sec > deadline->tv_sec) || + ((left->tv_sec == deadline->tv_sec) && (left->tv_nsec >= deadline->tv_nsec))) + return false; + + left->tv_sec = deadline->tv_sec - left->tv_sec; + left->tv_nsec = deadline->tv_nsec - left->tv_nsec; + if (left->tv_nsec < 0) + { + left->tv_nsec += 1000000000; + --left->tv_sec; + } + + // Limit the relative wait to one day, the deadline is checked again after it + if (left->tv_sec >= 86400) + left->tv_sec = 86400; + return true; + } + #endif /* PLATFORM_MACOSX */ Thread::Thread() { atomic_store(&enState, TS_CREATED); nResult = STATUS_OK; bCancelled = false; - CLR_HANDLE(hThread); + init_impl(); sBinding.proc = NULL; sBinding.arg = NULL; sBinding.runnable = NULL; @@ -80,7 +145,7 @@ namespace lsp atomic_store(&enState, TS_CREATED); nResult = STATUS_OK; bCancelled = false; - CLR_HANDLE(hThread); + init_impl(); sBinding.proc = proc; sBinding.arg = NULL; sBinding.runnable = NULL; @@ -91,7 +156,7 @@ namespace lsp atomic_store(&enState, TS_CREATED); nResult = STATUS_OK; bCancelled = false; - CLR_HANDLE(hThread); + init_impl(); sBinding.proc = proc; sBinding.runnable = NULL; sBinding.arg = arg; @@ -102,7 +167,7 @@ namespace lsp atomic_store(&enState, TS_CREATED); nResult = STATUS_OK; bCancelled = false; - CLR_HANDLE(hThread); + init_impl(); sBinding.proc = NULL; sBinding.arg = NULL; sBinding.runnable = runnable; @@ -110,12 +175,47 @@ namespace lsp Thread::~Thread() { + if (pImpl == NULL) + return; + #if defined(PLATFORM_WINDOWS) - if (hThread != INVALID_HANDLE_VALUE) - CloseHandle(hThread); + if (pImpl->hThread != INVALID_HANDLE_VALUE) + CloseHandle(pImpl->hThread); + DeleteCriticalSection(&pImpl->sLock); + #else + pthread_cond_destroy(&pImpl->sCancel); + pthread_mutex_destroy(&pImpl->sLock); #endif /* PLATFORM_WINDOWS */ - CLR_HANDLE(hThread); + delete pImpl; + pImpl = NULL; + } + + void Thread::init_impl() + { + pImpl = new detail::thread_impl_t; + if (pImpl == NULL) + return; + + #if defined(PLATFORM_WINDOWS) + pImpl->hThread = INVALID_HANDLE_VALUE; + InitializeCriticalSection(&pImpl->sLock); + InitializeConditionVariable(&pImpl->sCancel); + #else + pthread_mutex_init(&pImpl->sLock, NULL); + + // Use CLOCK_MONOTONIC for sleep() if possible, CLOCK_REALTIME otherwise. MacOS has no + // pthread_condattr_setclock() and waits for the time left until CLOCK_MONOTONIC deadline. + pthread_condattr_t attr; + pthread_condattr_init(&attr); + pImpl->nClockId = CLOCK_MONOTONIC; + #if !defined(PLATFORM_MACOSX) + if (pthread_condattr_setclock(&attr, CLOCK_MONOTONIC) != 0) + pImpl->nClockId = CLOCK_REALTIME; + #endif /* PLATFORM_MACOSX */ + pthread_cond_init(&pImpl->sCancel, &attr); + pthread_condattr_destroy(&attr); + #endif /* PLATFORM_WINDOWS */ } status_t Thread::run() @@ -138,22 +238,35 @@ namespace lsp return STATUS_BAD_STATE; } + // Set the flag and wake up the thread if it sleeps. The thread has been started, + // so pImpl is not NULL. + #if defined(PLATFORM_WINDOWS) + EnterCriticalSection(&pImpl->sLock); + bCancelled = true; + WakeConditionVariable(&pImpl->sCancel); + LeaveCriticalSection(&pImpl->sLock); + #else + pthread_mutex_lock(&pImpl->sLock); bCancelled = true; + pthread_cond_signal(&pImpl->sCancel); + pthread_mutex_unlock(&pImpl->sLock); + #endif /* PLATFORM_WINDOWS */ + return STATUS_OK; } #if defined(PLATFORM_WINDOWS) - DWORD WINAPI Thread::thread_launcher(_In_ LPVOID lpParameter) + DWORD WINAPI detail::thread_impl_t::launcher(_In_ LPVOID lpParameter) { Thread *_this = reinterpret_cast(lpParameter); - pThis = _this; + Thread::pThis = _this; // Wait until we are ready to launch while (!atomic_cas(&_this->enState, TS_PENDING, TS_RUNNING)) {} // Execute the thread status_t res = _this->run(); - pThis = NULL; + Thread::pThis = NULL; // Commit the 'FINISHED' status int state; @@ -168,12 +281,15 @@ namespace lsp status_t Thread::start() { + if (pImpl == NULL) + return STATUS_NO_MEM; + DWORD tid; - HANDLE thandle = CreateThread(NULL, 0, thread_launcher, this, 0, &tid); + HANDLE thandle = CreateThread(NULL, 0, detail::thread_impl_t::launcher, this, 0, &tid); if (thandle == INVALID_HANDLE_VALUE) return STATUS_UNKNOWN_ERR; - hThread = thandle; + pImpl->hThread = thandle; atomic_store(&enState, TS_PENDING); return STATUS_OK; } @@ -187,7 +303,7 @@ namespace lsp case TS_PENDING: case TS_RUNNING: { - DWORD res = WaitForSingleObject(hThread, INFINITE); + DWORD res = WaitForSingleObject(pImpl->hThread, INFINITE); if ((res != WAIT_OBJECT_0) && (res != WAIT_ABANDONED)) return STATUS_UNKNOWN_ERR; break; @@ -216,15 +332,36 @@ namespace lsp if (pThis->bCancelled) return STATUS_CANCELLED; - while (millis > 0) + // Wait until the timeout expires or the thread is cancelled + Thread *self = pThis; + detail::thread_impl_t *impl = self->pImpl; + status_t res = STATUS_OK; + wsize_t left = millis; + LARGE_INTEGER freq, start, now; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&start); + + EnterCriticalSection(&impl->sLock); + while ((!self->bCancelled) && (left > 0)) { - if (pThis->bCancelled) - return STATUS_CANCELLED; + const DWORD timeout = (left >= INFINITE) ? INFINITE - 1 : DWORD(left); + if ((!SleepConditionVariableCS(&impl->sCancel, &impl->sLock, timeout)) && + (GetLastError() != ERROR_TIMEOUT)) + { + res = STATUS_UNKNOWN_ERR; + break; + } - DWORD interval = (millis > 100) ? 100 : millis; - Sleep(interval); - millis -= interval; + // Compute the time left, the timeout may expire early and wake-ups may be spurious + QueryPerformanceCounter(&now); + const wsize_t passed = wsize_t(now.QuadPart - start.QuadPart) * 1000 / wsize_t(freq.QuadPart); + left = (passed < millis) ? millis - passed : 0; } + if (self->bCancelled) + res = STATUS_CANCELLED; + LeaveCriticalSection(&impl->sLock); + + return res; } return STATUS_OK; @@ -236,10 +373,10 @@ namespace lsp } #else - void *Thread::thread_launcher(void *arg) + void *detail::thread_impl_t::launcher(void *arg) { Thread *_this = reinterpret_cast(arg); - pThis = _this; + Thread::pThis = _this; // Cleanup cancellation state pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); @@ -264,11 +401,14 @@ namespace lsp status_t Thread::start() { + if (pImpl == NULL) + return STATUS_NO_MEM; + pthread_t tid; - if (pthread_create(&tid, NULL, &thread_launcher, this) != 0) + if (pthread_create(&tid, NULL, &detail::thread_impl_t::launcher, this) != 0) return STATUS_UNKNOWN_ERR; - hThread = tid; + pImpl->hThread = tid; atomic_store(&enState, TS_PENDING); return STATUS_OK; } @@ -281,7 +421,7 @@ namespace lsp return STATUS_BAD_STATE; case TS_PENDING: case TS_RUNNING: - if (pthread_join(hThread, NULL) != 0) + if (pthread_join(pImpl->hThread, NULL) != 0) return STATUS_UNKNOWN_ERR; break; case TS_FINISHED: @@ -314,28 +454,51 @@ namespace lsp if (pThis->bCancelled) return STATUS_CANCELLED; - while (millis > 0) - { - if (pThis->bCancelled) - return STATUS_CANCELLED; + // Compute the deadline + Thread *self = pThis; + detail::thread_impl_t *impl = self->pImpl; + status_t res = STATUS_OK; + struct timespec deadline; - wsize_t interval = (millis > 100) ? 100 : millis; - req.tv_sec = 0; - req.tv_nsec = interval * 1000000; + if (::clock_gettime(impl->nClockId, &deadline) != 0) + return STATUS_UNKNOWN_ERR; + add_millis(&deadline, millis); - while (::nanosleep(&req, &rem) != 0) + // Wait until the deadline is reached or the thread is cancelled + pthread_mutex_lock(&impl->sLock); + while (!self->bCancelled) + { + #if defined(PLATFORM_MACOSX) + struct timespec left; + if (::clock_gettime(CLOCK_MONOTONIC, &left) != 0) { - int code = errno; - if (code != EINTR) - return STATUS_UNKNOWN_ERR; - - if (pThis->bCancelled) - return STATUS_CANCELLED; - req = rem; + res = STATUS_UNKNOWN_ERR; + break; } - - millis -= interval; + if (!time_left(&left, &deadline)) + break; + const int code = pthread_cond_timedwait_relative_np(&impl->sCancel, &impl->sLock, &left); + if ((code != 0) && (code != ETIMEDOUT)) + { + res = STATUS_UNKNOWN_ERR; + break; + } + #else + const int code = pthread_cond_timedwait(&impl->sCancel, &impl->sLock, &deadline); + if (code == ETIMEDOUT) + break; + else if (code != 0) + { + res = STATUS_UNKNOWN_ERR; + break; + } + #endif /* PLATFORM_MACOSX */ } + if (self->bCancelled) + res = STATUS_CANCELLED; + pthread_mutex_unlock(&impl->sLock); + + return res; } return STATUS_OK; diff --git a/src/test/utest/ipc/thread_sleep.cpp b/src/test/utest/ipc/thread_sleep.cpp new file mode 100644 index 0000000..40bda19 --- /dev/null +++ b/src/test/utest/ipc/thread_sleep.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2026 Linux Studio Plugins Project + * (C) 2026 Vladimir Sadovnikov + * + * This file is part of lsp-runtime-lib + * Created on: 15 окт. 2026 г. + * + * lsp-runtime-lib is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * lsp-runtime-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with lsp-runtime-lib. If not, see . + */ + +#include +#include +#include + +#include +#include +#include + +#if defined(PLATFORM_WINDOWS) + #include +#else + #include +#endif /* PLATFORM_WINDOWS */ + +using namespace lsp; + +namespace +{ + static const wssize_t NANOS_PER_MILLI = 1000000; + static const wssize_t NANOS_PER_SECOND = 1000000000; + + // Time of the monotonic clock in nanoseconds, does not depend on system time changes + static wssize_t monotonic_nanos() + { + #if defined(PLATFORM_WINDOWS) + LARGE_INTEGER freq, now; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&now); + return (now.QuadPart / freq.QuadPart) * NANOS_PER_SECOND + + ((now.QuadPart % freq.QuadPart) * NANOS_PER_SECOND) / freq.QuadPart; + #else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return wssize_t(ts.tv_sec) * NANOS_PER_SECOND + ts.tv_nsec; + #endif /* PLATFORM_WINDOWS */ + } +} /* namespace */ + +UTEST_BEGIN("runtime.ipc", thread_sleep) + + UTEST_TIMELIMIT(30) + + class SleepThread: public ipc::Thread + { + public: + wsize_t nInterval; // Sleep interval, milliseconds + wssize_t nAlign; // Start of the sleep within a second of the monotonic clock, milliseconds + int nEntered; // Set just before the call of sleep() + wssize_t nStart; // Start of the sleep, nanoseconds + wssize_t nEnd; // End of the sleep, nanoseconds + ssize_t nSwitches; + status_t nStatus; + + public: + explicit SleepThread(wsize_t interval, wssize_t align = -1) + { + nInterval = interval; + nAlign = align; + nEntered = 0; + nStart = 0; + nEnd = 0; + nSwitches = -1; + nStatus = STATUS_OK; + } + + static ssize_t voluntary_switches() + { + ssize_t result = -1; + #if defined(PLATFORM_LINUX) + char path[64], line[128]; + snprintf(path, sizeof(path), "/proc/self/task/%lu/status", (unsigned long)(current_thread_id())); + FILE *fd = fopen(path, "r"); + if (fd == NULL) + return result; + while (fgets(line, sizeof(line), fd) != NULL) + { + if (strncmp(line, "voluntary_ctxt_switches:", 24) == 0) + { + result = atol(&line[24]); + break; + } + } + fclose(fd); + #endif /* PLATFORM_LINUX */ + return result; + } + + virtual status_t run() + { + // Wait until the monotonic clock is in the window [nAlign, nAlign + 50) ms of a second + while (nAlign >= 0) + { + const wssize_t millis = (monotonic_nanos() % NANOS_PER_SECOND) / NANOS_PER_MILLI; + const wssize_t delay = (nAlign + 1000 - millis) % 1000; + if (delay == 0) + break; + if (delay > 950) + break; + sleep(delay); + } + + const ssize_t before = voluntary_switches(); + nStart = monotonic_nanos(); + atomic_store(&nEntered, 1); + nStatus = sleep(nInterval); + nEnd = monotonic_nanos(); + const ssize_t after = voluntary_switches(); + if ((before >= 0) && (after >= 0)) + nSwitches = after - before; + return STATUS_OK; + } + }; + + UTEST_MAIN + { + // Cancel the sleeping thread. With the sleep split into 100 ms intervals, the thread + // would wake up 70..80 ms after cancel() for these offsets. The 40 ms limit of the + // wake-up leaves room for the scheduling delays of a loaded system or of valgrind. + static const wsize_t intervals[] = { 10000, 10000, ~wsize_t(0) }; + static const wsize_t offsets[] = { 20, 30, 130 }; + static const wssize_t max_latency = 40 * NANOS_PER_MILLI; + for (size_t i=0; i<3; ++i) + { + SleepThread t(intervals[i]); + UTEST_ASSERT(t.start() == STATUS_OK); + while (atomic_load(&t.nEntered) == 0) + ipc::Thread::sleep(1); + ipc::Thread::sleep(offsets[i]); + UTEST_ASSERT(!t.finished()); + + const wssize_t cancel = monotonic_nanos(); + UTEST_ASSERT(t.cancel() == STATUS_OK); + UTEST_ASSERT(t.join() == STATUS_OK); + const wssize_t joined = monotonic_nanos(); + + printf("Cancelled sleep: interval=%llu ms, status=%d, slept before cancel=%.3f ms, wake-up after cancel=%.3f ms, join after cancel=%.3f ms\n", + (unsigned long long)intervals[i], int(t.nStatus), + double(cancel - t.nStart) / NANOS_PER_MILLI, + double(t.nEnd - cancel) / NANOS_PER_MILLI, + double(joined - cancel) / NANOS_PER_MILLI); + UTEST_ASSERT(t.nStatus == STATUS_CANCELLED); + // The thread was inside sleep() when cancelled + UTEST_ASSERT(cancel - t.nStart >= wssize_t(offsets[i]) * NANOS_PER_MILLI); + UTEST_ASSERT(t.nEnd >= cancel); + UTEST_ASSERT(t.nEnd - cancel <= max_latency); + } + + // Sleep without cancel() should last the whole interval without waking up + { + SleepThread t(320); + UTEST_ASSERT(t.start() == STATUS_OK); + UTEST_ASSERT(t.join() == STATUS_OK); + + const wssize_t elapsed = t.nEnd - t.nStart; + printf("Full sleep: status=%d, slept=%.3f ms, voluntary context switches=%d\n", + int(t.nStatus), double(elapsed) / NANOS_PER_MILLI, int(t.nSwitches)); + UTEST_ASSERT(t.nStatus == STATUS_OK); + UTEST_ASSERT(elapsed >= 320 * NANOS_PER_MILLI); + #if defined(PLATFORM_LINUX) + if (t.nSwitches >= 0) + UTEST_ASSERT(t.nSwitches <= 2); + else + printf("Voluntary context switch check skipped: /proc not readable\n"); + #endif /* PLATFORM_LINUX */ + } + + // Sleep that ends in the next second of the monotonic clock (with the nanosecond carry) + // and in the same second (without it) + static const wssize_t aligns[] = { 900, 100 }; + for (size_t i=0; i<2; ++i) + { + SleepThread t(150, aligns[i]); + UTEST_ASSERT(t.start() == STATUS_OK); + UTEST_ASSERT(t.join() == STATUS_OK); + + const wssize_t start = (t.nStart % NANOS_PER_SECOND) / NANOS_PER_MILLI; + const wssize_t elapsed = t.nEnd - t.nStart; + printf("Aligned sleep: status=%d, start=%d ms of the second, slept=%.3f ms\n", + int(t.nStatus), int(start), double(elapsed) / NANOS_PER_MILLI); + UTEST_ASSERT(t.nStatus == STATUS_OK); + UTEST_ASSERT(elapsed >= 150 * NANOS_PER_MILLI); + UTEST_ASSERT(elapsed <= 250 * NANOS_PER_MILLI); + #if !defined(PLATFORM_WINDOWS) + UTEST_ASSERT((start >= aligns[i]) && (start < aligns[i] + 50)); + #endif /* PLATFORM_WINDOWS */ + } + } + +UTEST_END