Ping-pong
Two tasks hand the processor back and forth a million times. Each Release wakes the other task,
each Wait suspends the calling one, so every round trip is two suspends and two resumes. The
program measures how many round trips it manages per second – a direct look at the cost of
suspending and resuming a task.
The two tasks
// Each semaphore has exactly one waiter - the task it wakes - so the single-waiter
// variant fits.
class PingTask : public TC::Tasks::Task
{
public:
PingTask(Semaphore_s& ping, Semaphore_s& pong) noexcept
: ping_(ping),
pong_(pong)
{}
void Run() noexcept override
{
for (std::size_t i = 0; i < RoundTrips; ++i) {
pong_.Release(); // never suspends
ping_.Wait(); // suspends until the pong task answers
++count;
}
}
std::size_t count = 0;
private:
Semaphore_s& ping_;
Semaphore_s& pong_;
};
class PongTask : public TC::Tasks::Task
{
public:
PongTask(Semaphore_s& ping, Semaphore_s& pong) noexcept
: ping_(ping),
pong_(pong)
{}
void Run() noexcept override
{
for (std::size_t i = 0; i < RoundTrips; ++i) {
pong_.Wait();
ping_.Release();
++count;
}
}
std::size_t count = 0;
private:
Semaphore_s& ping_;
Semaphore_s& pong_;
};Release adds a permit and resumes the waiting task, if there is one; it
never suspends. Wait takes a permit, suspending the task while there is
none. A permit released before the other side waits is not lost: the next Wait takes it at
once.
Running them
// Two threads, so that a Release can wake the other task on the other thread.
// With one thread both tasks share it and every round trip is two plain task
// switches.
TC::SchedulerInfo info{.numberOfThreads = 2};
TC::ITaskScheduler* scheduler
= TC::CreateTaskScheduler(TC::SchedulerKind::Default, &info);
if (scheduler == nullptr)
return 1;
scheduler->Start();
bool ok = false;
{
// The semaphores, the tasks and the group outlive the tasks: they are
// destroyed only after Stop.
Semaphore_s ping;
Semaphore_s pong;
PingTask pingTask(ping, pong);
PongTask pongTask(ping, pong);
TC::Synchronization::TaskGroup_s group; // main is its only waiter
group.AddTask(
&pingTask); // before AddTask: the group must count the task in first
group.AddTask(&pongTask);
const auto start = std::chrono::steady_clock::now();
scheduler->AddTask(&pingTask);
scheduler->AddTask(&pongTask);
group.Wait();
const std::chrono::duration<double> elapsed
= std::chrono::steady_clock::now() - start;
scheduler->Stop();
ok = pingTask.count == RoundTrips && pongTask.count == RoundTrips;
std::printf("%zu round trips in %.3f s: %.1f million per second (%s)\n",
RoundTrips, elapsed.count(),
static_cast<double>(RoundTrips) / elapsed.count() / 1e6,
ok ? "ok" : "MISMATCH");
}The TaskGroup_s counts both tasks in before they are added to the scheduler and
lets main pass once both have exited.
Full program
// Ping-pong: two tasks hand the processor back and forth through two semaphores and
// count the round trips per second.
#include <Windows.h>
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <ThinkMeta.ConcurrentTasks/Core.h>
import ThinkMeta.ConcurrentTasks.Synchronization.Semaphore;
import ThinkMeta.ConcurrentTasks.Synchronization.TaskGroup;
import ThinkMeta.ConcurrentTasks.Tasks.Task;
namespace TC = ThinkMeta::ConcurrentTasks;
using TC::Synchronization::Semaphore_s;
constexpr std::size_t RoundTrips = 1'000'000;
// Each semaphore has exactly one waiter - the task it wakes - so the single-waiter
// variant fits.
class PingTask : public TC::Tasks::Task
{
public:
PingTask(Semaphore_s& ping, Semaphore_s& pong) noexcept
: ping_(ping),
pong_(pong)
{}
void Run() noexcept override
{
for (std::size_t i = 0; i < RoundTrips; ++i) {
pong_.Release(); // never suspends
ping_.Wait(); // suspends until the pong task answers
++count;
}
}
std::size_t count = 0;
private:
Semaphore_s& ping_;
Semaphore_s& pong_;
};
class PongTask : public TC::Tasks::Task
{
public:
PongTask(Semaphore_s& ping, Semaphore_s& pong) noexcept
: ping_(ping),
pong_(pong)
{}
void Run() noexcept override
{
for (std::size_t i = 0; i < RoundTrips; ++i) {
pong_.Wait();
ping_.Release();
++count;
}
}
std::size_t count = 0;
private:
Semaphore_s& ping_;
Semaphore_s& pong_;
};
int main()
{
TC::ConvertThreadToTask();
// Two threads, so that a Release can wake the other task on the other thread.
// With one thread both tasks share it and every round trip is two plain task
// switches.
TC::SchedulerInfo info{.numberOfThreads = 2};
TC::ITaskScheduler* scheduler
= TC::CreateTaskScheduler(TC::SchedulerKind::Default, &info);
if (scheduler == nullptr)
return 1;
scheduler->Start();
bool ok = false;
{
// The semaphores, the tasks and the group outlive the tasks: they are
// destroyed only after Stop.
Semaphore_s ping;
Semaphore_s pong;
PingTask pingTask(ping, pong);
PongTask pongTask(ping, pong);
TC::Synchronization::TaskGroup_s group; // main is its only waiter
group.AddTask(
&pingTask); // before AddTask: the group must count the task in first
group.AddTask(&pongTask);
const auto start = std::chrono::steady_clock::now();
scheduler->AddTask(&pingTask);
scheduler->AddTask(&pongTask);
group.Wait();
const std::chrono::duration<double> elapsed
= std::chrono::steady_clock::now() - start;
scheduler->Stop();
ok = pingTask.count == RoundTrips && pongTask.count == RoundTrips;
std::printf("%zu round trips in %.3f s: %.1f million per second (%s)\n",
RoundTrips, elapsed.count(),
static_cast<double>(RoundTrips) / elapsed.count() / 1e6,
ok ? "ok" : "MISMATCH");
}
scheduler->Destroy();
TC::ConvertTaskToThread();
return ok ? 0 : 1;
}Build and run
A console program. With the core's props file imported and the library referenced (see Set up a project) it needs nothing else. It runs well under a second; the rate depends on the machine:
1000000 round trips in 0.700 s: 1.4 million per second (ok)What to notice
- One waiter per semaphore. Only the ping task waits on
ping, only the pong task onpong, so the cheaper Semaphore_s fits. With two tasks waiting on the same_sobject the program would be invalid – see Single-waiter variants. - Threads matter. The program creates two scheduler threads, so a
Releasemay resume the other task on the other thread. WithnumberOfThreads = 1both tasks share one thread and every round trip is two plain task switches; change the setting and compare the rates on your machine. - Lifetimes. The semaphores, the tasks and the group are destroyed only after Stop: an object a task waits on, signals or reports its exit to must outlive that task. See Lifetimes.
- Count in first. AddTask on the group comes before AddTask on the scheduler, so a task cannot exit before it is counted.