- Use a ''monostate'' as access point to thread-local service instances - rely on a configuration-context to setup the actual service - investigate runtime overhead using a multithreaded microbenchmark
204 lines
6.1 KiB
C++
204 lines
6.1 KiB
C++
/*
|
||
LocalSlice(Test) - verify access to thread-local services
|
||
|
||
Copyright (C)
|
||
2026, Hermann Vosseler <Ichthyostega@web.de>
|
||
|
||
**Lumiera** 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. See the file COPYING for further details.
|
||
|
||
* *****************************************************************/
|
||
|
||
/** @file local-slice-test.cpp
|
||
** unit test \ref LocalSlice_test
|
||
*/
|
||
|
||
#include "test/run.hpp"
|
||
#include "test/test-helper.hpp"
|
||
#include "lib/scoped-collection.hpp"
|
||
#include "test/microbenchmark.hpp"
|
||
#include "lib/iter-explorer.hpp"
|
||
#include "lib/format-cout.hpp"
|
||
#include "lib/thread.hpp"
|
||
|
||
#include "lib/local-slice.hpp"
|
||
|
||
using lib::Thread;
|
||
using lib::explore;
|
||
using test::threadBenchmark;
|
||
using std::this_thread::yield;
|
||
using std::this_thread::sleep_for;
|
||
using std::chrono_literals::operator ""us;
|
||
|
||
|
||
namespace lib {
|
||
namespace test {
|
||
|
||
namespace { // test fixture...
|
||
|
||
const uint NUM_THREADS = 200;
|
||
|
||
constexpr size_t CONCURR_BENCH = 4;
|
||
const size_t REPETITIONS_BENCH = 1'000'000;
|
||
|
||
/**
|
||
* the "service" to be instantiated thread-local
|
||
*/
|
||
class DummyBackend
|
||
{
|
||
uint step{0};
|
||
public:
|
||
uint
|
||
getStep()
|
||
{
|
||
return ++step;
|
||
}
|
||
};
|
||
|
||
struct DummySetup
|
||
{
|
||
using Service = DummyBackend;
|
||
|
||
static Service init() { return Service(); }
|
||
};
|
||
|
||
using DummySlice = LocalSlice<DummySetup>;
|
||
|
||
|
||
/**
|
||
* A common shared service instance
|
||
* that dispatches through LocalSlice
|
||
* into the thread-local service back-ends
|
||
*/
|
||
class DummyFrontend
|
||
{
|
||
DummySlice dummySrv_;
|
||
|
||
public:
|
||
uint
|
||
invoke()
|
||
{
|
||
return dummySrv_->getStep()
|
||
+ dummySrv_->getStep()
|
||
+ dummySrv_->getStep()
|
||
+ dummySrv_->getStep()
|
||
;
|
||
}
|
||
};
|
||
//
|
||
} //(End) test fixture
|
||
|
||
|
||
/***********************************************************************//**
|
||
* @test demonstrate access to thread-local service instances.
|
||
* - setup a common front-end service instance
|
||
* - its functionality is implemented by delegating
|
||
* through the LocalSlice into the thread-local back-ends
|
||
* - launch threads and invoke the front-end concurrently
|
||
* @see local-slice.hpp
|
||
*/
|
||
class LocalSlice_test : public Test
|
||
{
|
||
|
||
virtual void
|
||
run (Arg)
|
||
{
|
||
simpleUsage();
|
||
investigatePerformance();
|
||
}
|
||
|
||
|
||
/** @test access thread-local service back-ends
|
||
* through a shared front-end instance;
|
||
* verify that each thread invokes a
|
||
* separate instance without interference
|
||
* @see ThreadWrapper_test
|
||
*/
|
||
void
|
||
simpleUsage ()
|
||
{
|
||
// Σi ≡ N/2·(N+1)
|
||
auto expectSum = [](uint n){ return n/2 * (n+1); };
|
||
|
||
// the front-end invokes the back-end four times in row...
|
||
uint i1 = expectSum(4);
|
||
uint i2 = expectSum(8) - expectSum(4);
|
||
uint i3 = expectSum(12) - expectSum(8);
|
||
|
||
// Note: all threads use this common service front-end instance
|
||
DummyFrontend myDummy;
|
||
|
||
// Now invoke through this front-end
|
||
CHECK (i1 == myDummy.invoke());
|
||
CHECK (i2 == myDummy.invoke());
|
||
|
||
// launch a significant number of threads,
|
||
// each verifying the same invocation sequence
|
||
lib::ScopedCollection<Thread> threads{NUM_THREADS};
|
||
for (uint i=1; i<=NUM_THREADS; ++i)
|
||
threads.emplace ([&]{
|
||
CHECK (i1 == myDummy.invoke());
|
||
|
||
sleep_for (100us);
|
||
|
||
CHECK (i2 == myDummy.invoke());
|
||
|
||
sleep_for (100us);
|
||
|
||
CHECK (i3 == myDummy.invoke());
|
||
});
|
||
|
||
while (explore(threads).has_any())
|
||
yield(); // wait for all threads to finish
|
||
|
||
|
||
// ...without any interference with the main thread's instance
|
||
CHECK (i3 == myDummy.invoke());
|
||
}
|
||
|
||
|
||
|
||
/** @test investigate performance of thread-local dispatch
|
||
* - subject-1 accesses the increment through the common thread-local dispatch
|
||
* - subject-2 increments just a local variable in each thread
|
||
* - run a multithreaded benchmark with 1 million repetitions
|
||
* - but only small number of threads to avoid OS scheduler congestion
|
||
* @see test::threadBenchmark()
|
||
*/
|
||
void
|
||
investigatePerformance()
|
||
{
|
||
DummySlice threadLocalBackend;
|
||
auto testThreadLocal = [&]{
|
||
auto volatile result = threadLocalBackend->getStep();
|
||
return result;
|
||
};
|
||
|
||
auto testLocalInc = [cnt=0] mutable
|
||
{
|
||
auto volatile result = ++cnt;
|
||
return result;
|
||
};
|
||
|
||
cout << "µ-benchmark :: thread-local" <<endl;
|
||
|
||
auto [t1, s1] = ::test::threadBenchmark<CONCURR_BENCH> (testThreadLocal, REPETITIONS_BENCH);
|
||
cout << "thread-local dispatch: "<<t1<<"µs" <<endl;
|
||
|
||
auto [t2, s2] = ::test::threadBenchmark<CONCURR_BENCH> (testLocalInc, REPETITIONS_BENCH);
|
||
cout << "increment local var : "<<t2<<"µs" <<endl;
|
||
|
||
CHECK (s1 == s2);
|
||
CHECK (t1 > t2);
|
||
}
|
||
|
||
};
|
||
|
||
|
||
/** Register this test class... */
|
||
LAUNCHER(LocalSlice_test, "unit common");
|
||
|
||
|
||
}} // namespace lib::test
|