Sane C++ Libraries
C++ Platform Abstraction Libraries
Loading...
Searching...
No Matches
Threading

🟩 Atomic, thread, thread pool, mutex, semaphore, barrier, rw-lock, condition variable

Threading is a library defining basic primitives for user-space threading and synchronization.

Dependencies

Statistics

Type Lines Of Code Comments Sum
Headers 407 249 656
Sources 917 168 1085
Sum 1324 417 1741

Features

Class Description
SC::Thread A native OS thread.
SC::ThreadPool Simple thread pool that executes tasks in a fixed number of worker threads.
SC::Mutex A native OS mutex to synchronize access to shared resources.
SC::RWLock A Read-Write lock that allows multiple concurrent readers but only one writer.
SC::Barrier A synchronization point that blocks threads until the required number of threads have reached it.
SC::Semaphore A semaphore synchronization primitive that maintains a count for resource management.
SC::ConditionVariable A native OS condition variable.
SC::Atomic Atomic variables (only for int and bool for now).
SC::EventObject An automatically reset event object to synchronize two threads.

Status

🟩 Usable
All the main threading primitives are there.
The Atomic header is really only being implemented for a few data types and needs some love to extend and improve it.

Blog

Some relevant blog posts are:

Videos

This is the list of videos that have been recorded showing some of the internal thoughts that have been going into this library:

SC::Thread

A native OS thread.

Example:

Thread thread;
thread.start([](Thread& thread)
{
// It's highly recommended setting a name for the thread
thread.setThreadName(SC_NATIVE_STR("My Thread"));
// Do something on the thread
Thread::Sleep(1000); // Sleep for 1 second
});
thread.join(); // wait until thread has finished executing
// ...or
thread.detach(); // To keep thread running after Thread destructor
Warning
Thread destructor will assert if SC::Thread::detach() or SC::Thread::join() has not been called.

SC::ThreadPool

Simple thread pool that executes tasks in a fixed number of worker threads.

This class is not copyable / moveable due to it containing Mutex and Condition variable. Additionally, this class does not allocate any memory by itself, and expects the caller to supply SC::ThreadPool::Task objects.

Warning
The caller is responsible of keeping Task address stable until the it will be completed. If it's not already completed the task must still be valid during ThreadPool::destroy or ThreadPool destructor.

Example:

static const size_t wantedThreads = 4;
static const size_t numTasks = 100;
// 1. Create the threadpool with the wanted number of threads
SC::ThreadPool threadPool;
SC_TEST_EXPECT(threadPool.create(wantedThreads));
size_t values[numTasks];
// 2. Allocate the wanted number of tasks. Tasks memory should be valid until a task is finished.
SC::ThreadPool::Task tasks[numTasks];
for (size_t idx = 0; idx < numTasks; idx++)
{
size_t* value = values + idx;
*value = idx;
// 3. Setup the task function to execute on some random thread
tasks[idx].function = [value]()
{
if (*value % 2)
{
}
*value *= 100;
};
// 4. Queue the task in thread pool
SC_TEST_EXPECT(threadPool.queueTask(tasks[idx]));
}
// 5. [Optional] Wait for a single task
SC_TEST_EXPECT(threadPool.waitForTask(tasks[1]));
SC_TEST_EXPECT(values[1] == 100);
// 6. [Optional] Wait for all remaining tasks to be finished
// Checking Results
bool allGood = true;
for (size_t idx = 0; idx < numTasks; idx++)
{
allGood = allGood && (values[idx] == idx * 100);
}
SC_TEST_EXPECT(allGood);
// 6. [Optional] Destroy the threadpool.
// Note: destructor will wait for tasks to finish, but this avoids it from accessing invalid tasks,
// as stack objects are reclaimed in inverse declaration order
SC_TEST_EXPECT(threadPool.destroy());

SC::Mutex

A native OS mutex to synchronize access to shared resources.

Example:

Mutex mutex;
int globalVariable = 0;
Thread thread1;
auto thread1Func = [&](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Thread1"));
mutex.lock();
globalVariable++;
mutex.unlock();
};
SC_TEST_EXPECT(thread1.start(thread1Func));
Thread thread2;
auto thread2Func = [&](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Signaling2"));
mutex.lock();
globalVariable++;
mutex.unlock();
};
SC_TEST_EXPECT(thread2.start(thread2Func));
SC_TEST_EXPECT(thread1.join());
SC_TEST_EXPECT(thread2.join());
SC_TEST_EXPECT(globalVariable == 2);

SC::EventObject

An automatically reset event object to synchronize two threads.


Example:

EventObject eventObject;
Thread threadWaiting;
auto waitingFunc = [&](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Thread waiting"));
eventObject.wait();
report.console.printLine("After waiting");
};
SC_TEST_EXPECT(threadWaiting.start(waitingFunc));
Thread threadSignaling;
auto signalingFunc = [&](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Signaling thread"));
report.console.printLine("Signal");
eventObject.signal();
};
SC_TEST_EXPECT(threadSignaling.start(signalingFunc));
SC_TEST_EXPECT(threadWaiting.join());
SC_TEST_EXPECT(threadSignaling.join());
// Prints:
// Signal
// After waiting

SC::Atomic

Atomic variables (only for int and bool for now).


Example:

Atomic<bool> test = true;
SC_TEST_EXPECT(test.load());
test.exchange(false);
SC_TEST_EXPECT(not test.load());
#define SC_TEST_EXPECT(e)
Records a test expectation (eventually aborting or breaking o n failed test)
Definition Testing.h:119

SC::Semaphore

A semaphore synchronization primitive that maintains a count for resource management.

Example:

constexpr int maxResources = 2; // Only 2 threads can access resource at once
constexpr int numThreads = 4; // Total number of threads trying to access
constexpr int operationsPerThread = 3; // Each thread will do 3 operations
Semaphore semaphore(maxResources); // Initialize with 2 available resources
struct Context
{
Semaphore& semaphore;
Mutex counterMutex; // To protect sharedResource counter
int sharedResource = 0; // Counter to verify correct synchronization
} ctx{semaphore, {}};
Thread threads[numThreads];
for (int i = 0; i < numThreads; i++)
{
auto threadFunc = [this, &ctx](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Worker Thread"));
for (int j = 0; j < operationsPerThread; j++)
{
ctx.semaphore.acquire(); // Wait for resource to be available
// Critical section
ctx.counterMutex.lock();
ctx.sharedResource++;
SC_TEST_EXPECT(ctx.sharedResource <= maxResources); // Never more than maxResources threads
Thread::Sleep(1); // Simulate some work
ctx.sharedResource--;
ctx.counterMutex.unlock();
ctx.semaphore.release(); // Release the resource
Thread::Sleep(1); // Give other threads a chance
}
};
SC_TEST_EXPECT(threads[i].start(threadFunc));
}
// Wait for all threads to finish
for (int i = 0; i < numThreads; i++)
{
SC_TEST_EXPECT(threads[i].join());
}
// Verify final state
SC_TEST_EXPECT(ctx.sharedResource == 0);

SC::RWLock

A Read-Write lock that allows multiple concurrent readers but only one writer.

Example:

constexpr int numReaders = 3;
constexpr int numIterations = 100;
RWLock rwlock;
int sharedData = 0;
// Start multiple reader threads
Thread readers[numReaders];
for (int i = 0; i < numReaders; i++)
{
auto readerFunc = [&](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Reader"));
for (int j = 0; j < numIterations; j++)
{
rwlock.lockRead();
volatile int value = sharedData; // Prevent optimization
(void)value;
rwlock.unlockRead();
Thread::Sleep(1); // Small delay to increase contention
}
};
SC_TEST_EXPECT(readers[i].start(readerFunc));
}
// Start a writer thread
Thread writer;
auto writerFunc = [&](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Writer"));
for (int i = 0; i < numIterations; i++)
{
rwlock.lockWrite();
sharedData++;
rwlock.unlockWrite();
Thread::Sleep(1); // Small delay to increase contention
}
};
SC_TEST_EXPECT(writer.start(writerFunc));
// Wait for all threads to finish
for (int i = 0; i < numReaders; i++)
{
SC_TEST_EXPECT(readers[i].join());
}
SC_TEST_EXPECT(writer.join());
SC_TEST_EXPECT(sharedData == numIterations);

SC::Barrier

A synchronization point that blocks threads until the required number of threads have reached it.

Example:

constexpr uint32_t numThreads = 8;
constexpr int incrementsPerThread = 1000;
Thread threads[numThreads];
Barrier barrier(numThreads);
struct Context
{
Barrier& barrier;
Atomic<int32_t> sharedCounter;
} ctx = {barrier, 0};
for (uint32_t i = 0; i < numThreads; i++)
{
auto threadFunc = [this, &ctx](Thread& thread)
{
thread.setThreadName(SC_NATIVE_STR("Barrier"));
// Phase 1: Each thread increments the counter
for (int j = 0; j < incrementsPerThread; ++j)
{
ctx.sharedCounter++;
}
ctx.barrier.wait();
// Phase 2: All threads should see the final value
SC_TEST_EXPECT(ctx.sharedCounter == numThreads * incrementsPerThread);
ctx.barrier.wait();
};
SC_TEST_EXPECT(threads[i].start(threadFunc));
}
// Wait for all threads to finish
for (uint32_t i = 0; i < numThreads; i++)
{
SC_TEST_EXPECT(threads[i].join());
}

SC::ConditionVariable

A native OS condition variable.

Roadmap

🟦 Complete Features:

  • Support more types in Atomic<T>