On a close look, the wrapper design as pursued here turns out to be prone to insidious data race problems. This was true also for the existing solution, but becomes more clear due to the precise definitions from the C++ standard. This is a confusing situation, because these races typically do not materialise in practice; due to the latency of the OS scheduler the new thread starts invoking user code at least 100µs after the Wrapper object is fully constructed (typically more like 500µs, which is a lot) The standard case (lib::Thread) in its current form is correct, but borderline to undefined behaviour, and any initialisation of members in a derived class would be off limits (the thread-wrapper should not be used as baseclass, rather as member)
158 lines
5.4 KiB
C++
158 lines
5.4 KiB
C++
/*
|
|
ThreadWrapperLifecycle(Test) - verify lifecycle aspects of the thread wrapper
|
|
|
|
Copyright (C) Lumiera.org
|
|
2023, Hermann Vosseler <Ichthyostega@web.de>
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License as
|
|
published by the Free Software Foundation; either version 2 of
|
|
the License, or (at your option) any later version.
|
|
|
|
This program 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 General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
|
|
* *****************************************************/
|
|
|
|
/** @file thread-wrapper-lifecycle-test.cpp
|
|
** unit test \ref ThreadWrapperLifecycle_test
|
|
*/
|
|
|
|
|
|
#include "lib/test/run.hpp"
|
|
#include "lib/thread.hpp"
|
|
#include "lib/iter-explorer.hpp"
|
|
#include "lib/scoped-collection.hpp"
|
|
#include "lib/test/microbenchmark.hpp"
|
|
#include "lib/test/diagnostic-output.hpp"
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
|
|
using test::Test;
|
|
using lib::explore;
|
|
using std::atomic_uint;
|
|
using std::this_thread::yield;
|
|
using std::this_thread::sleep_for;
|
|
using std::chrono::microseconds;
|
|
using std::chrono::system_clock;
|
|
|
|
|
|
namespace lib {
|
|
namespace test{
|
|
|
|
namespace { // test parameters
|
|
|
|
const uint NUM_THREADS = 200;
|
|
const uint REPETITIONS = 10;
|
|
|
|
using CLOCK_SCALE = std::micro; // Results are in µ-sec
|
|
}
|
|
|
|
|
|
/*******************************************************************//**
|
|
* @test verify lifecycle behaviour of threads managed by thread-wrapper.
|
|
* @see thread.hpp
|
|
* @see ThreadWrapperBackground_test
|
|
* @see ThreadWrapperJoin_test
|
|
*/
|
|
class ThreadWrapperLifecycle_test : public Test
|
|
{
|
|
|
|
virtual void
|
|
run (Arg)
|
|
{
|
|
defaultWrapperLifecycle();
|
|
verifyExplicitLifecycleState();
|
|
}
|
|
|
|
|
|
/** @test demonstrate terms of lifecycle for the default case */
|
|
void
|
|
defaultWrapperLifecycle()
|
|
{
|
|
using Dur = std::chrono::duration<double, CLOCK_SCALE>;
|
|
using Point = system_clock::time_point;
|
|
Point threadStart;
|
|
Point afterCtor;
|
|
Thread thread("lifecycle", [&]{
|
|
threadStart = system_clock::now();
|
|
});
|
|
afterCtor = system_clock::now();
|
|
while (thread) yield();
|
|
|
|
double offset = Dur{threadStart - afterCtor}.count();
|
|
SHOW_EXPR(offset)
|
|
CHECK (offset > 0);
|
|
UNIMPLEMENTED ("demonstrate state change");
|
|
}
|
|
|
|
|
|
/**
|
|
* @test verify a special setup to start a thread explicitly and to track
|
|
* the thread's lifecycle state.
|
|
*/
|
|
void
|
|
verifyExplicitLifecycleState()
|
|
{
|
|
struct TestThread
|
|
: Thread
|
|
{
|
|
using Thread::Thread;
|
|
|
|
uint local{0};
|
|
|
|
void
|
|
doIt (uint a, uint b) ///< the actual operation running in a separate thread
|
|
{
|
|
uint sum = a + b;
|
|
sleep_for (microseconds{sum}); // Note: explicit random delay before local store
|
|
local = sum;
|
|
}
|
|
};
|
|
|
|
// prepare Storage for these objects (not created yet)
|
|
lib::ScopedCollection<TestThread> threads{NUM_THREADS};
|
|
|
|
size_t checkSum = 0;
|
|
size_t globalSum = 0;
|
|
auto launchThreads = [&]
|
|
{
|
|
for (uint i=1; i<=NUM_THREADS; ++i)
|
|
{
|
|
uint x = rand() % 1000;
|
|
globalSum += (i + x);
|
|
threads.emplace (&TestThread::doIt, i, x);
|
|
} // Note: bind to member function, copying arguments
|
|
|
|
while (explore(threads).has_any())
|
|
yield(); // wait for all threads to have detached
|
|
|
|
for (auto& t : threads)
|
|
{
|
|
CHECK (0 < t.local);
|
|
checkSum += t.local;
|
|
}
|
|
};
|
|
|
|
double runTime = benchmarkTime (launchThreads, REPETITIONS);
|
|
|
|
CHECK (checkSum == globalSum); // sum of precomputed random numbers matches sum from threads
|
|
CHECK (runTime < NUM_THREADS * 1000/2); // random sleep time should be > 500ms on average
|
|
}
|
|
};
|
|
|
|
|
|
|
|
/** Register this test class... */
|
|
LAUNCHER (ThreadWrapperLifecycle_test, "function common");
|
|
|
|
|
|
|
|
}} // namespace lib::test
|