12 ManualResetEvent

Implementation of the equivalent to a manual reset Windows event object in terms of C++11.

ManualResetEvent.h

// ManualResetEvent.h
//
// Author David Barrett-Lennard
// (C)opyright Cedanet Pty Ltd 2013

#pragma once
#ifndef Ceda_cxThread_ManualResetEvent_H
#define Ceda_cxThread_ManualResetEvent_H

#include "cxThread.h"
#include "Ceda/cxUtils/CedaAssert.h"
#include <mutex>
#include <condition_variable>

namespace ceda
{
class ManualResetEvent
{
    cxNotCloneable(ManualResetEvent)

public:
    explicit ManualResetEvent(bool signaled = false) : signaled_(signaled) {}

    void Signal()
    {
        std::lock_guard<std::mutex> lock(mutex_);
        signaled_ = true;
        cv_.notify_all();
    }

    void Reset()
    {
        std::lock_guard<std::mutex> lock(mutex_);
        signaled_ = false;
    }

    void Wait()
    {
        std::unique_lock<std::mutex> lock(mutex_);
        cv_.wait(lock,[this]{return signaled_;});
    }

    // Wait with a timeout.
    // Returns true if the wait was completed successfully because the event had been signalled
    bool Wait(uint32 msec)
    {
        std::unique_lock<std::mutex> lock(mutex_);
        return cv_.wait_for(lock,std::chrono::milliseconds(msec),[this]{return signaled_;});
    }

private:
    std::mutex mutex_;
    std::condition_variable cv_;
    bool signaled_;
};
} // namespace ceda
#endif // include guard

Performance measurement

The performance of ManualResetEvent is measured in ThreadTests.cpp in txThread.

The following results were obtained with the windows-x64 build running under Windows 10 on a desktop with an AMD Ryzen 9 3900X 12-Core Processor:

Thread timing tests
    std::thread::hardware_concurrency() = 24
    Increment std::atomic<long> with multiple threads            (avg of 54) 100000/0.00115s =    86.87 MHz
    Increment std::atomic<long>                                  (avg of 155) 100000/0.0004s =    250.1 MHz
    Construct/Destruct std::mutex                                (avg of 74) 100000/0.000841s =    118.9 MHz
    Lock std::mutex                                              (avg of 35) 100000/0.00177s =    56.44 MHz
    Construct/Destruct ManualResetEvent                          (avg of 43) 100000/0.00147s =    68.12 MHz
    Signal(),Wait(),Reset() on ManualResetEvent                  (avg of 101) 10000/0.000612s =    16.33 MHz
    Read and write thread_local int*                             (avg of 130) 1e+06/0.000476s =    2.099 GHz

ThreadTests.cpp

// ThreadTests.cpp
//
// Author David Barrett-Lennard
// (C)opyright Cedanet Pty Ltd 2006

#include "Ceda/cxThread/ManualResetEvent.h"
#include "Ceda/cxUtils/TestTimer.h"
#include "Ceda/cxUtils/Tracer.h"
#include <thread>
#include <mutex>
#include <atomic>
#include <functional>

void execute_task()
{
    //std::cout << "Hello, world" << std::endl;
}

void test1()
{
    std::thread t(execute_task);
    t.join();   // wait until call to execute_task() completes
}

void test2()
{
    std::thread t[16];

    for (int i=0 ; i < 16 ; ++i)
        t[i] = std::thread(execute_task);

    for (int i=0 ; i < 16 ; ++i)
        t[i].join();
}

template <int N>
class Threads
{
public:
    Threads() {}
    Threads(std::function<void()> f)
    {
        for (int i=0 ; i < N ; ++i)
            threads_[i] = std::thread(f);
    }
    void join()
    {
        for (int i=0 ; i < N ; ++i)
            threads_[i].join();
    }
private:
    std::thread threads_[N];
};

void test3()
{
    Threads<16> t(execute_task);
    t.join();
}

class ObjectThatExecutesATask
{
public:
    ObjectThatExecutesATask() : running_(false) {}

    void ExecuteTask()
    {
    }

    void Start()
    {
        running_ = true;
        thread_ = std::thread
        (
            [this]
            {
                ExecuteTask();        
                std::lock_guard<std::mutex> lock(mutex_);
                running_ = false;
                cv_.notify_one();
            }
        );
    }

    void Stop()
    {
        std::unique_lock<std::mutex> lock(mutex_);
        cv_.wait(lock,[this]{return !running_;});
    }

private:
    std::thread thread_;
    std::mutex mutex_;
    std::condition_variable cv_;
    bool running_;
};

void test4()
{
    ObjectThatExecutesATask x;
    x.Start();
    x.Stop();
}

///////////////////////////////////////////////////////////////////////////////////////////////////

/*
Q9400 machine Release x64
    std::thread::hardware_concurrency() = 4
    Increment std::atomic<long> with multiple threads      1.73 s     23.08 MHz
    Increment std::atomic<long>                           0.761 s     131.4 MHz
    std::mutex                                            0.869 s     11.51 MHz
    Read and write TLS                                    0.676 s     147.9 MHz
*/

void ThreadTimingTests(double timeForTestInSecs)
{
    Tracer() << "Thread timing tests\n";
    ceda::TraceIndenter indent(4);

    double timePerTest = timeForTestInSecs / 18;

    // Note: std::thread::hardware_concurrency() is only a hint, and the function might return 0 
    // if this information is not available
    Tracer() << "std::thread::hardware_concurrency() = " << std::thread::hardware_concurrency() << '\n';

    ceda::TimeCode("Increment std::atomic<long> with multiple threads", timePerTest, 10000000, 100000000, 
        [](int n) 
        { 
            const int NUMTHREADS = 4;
            std::thread threads[NUMTHREADS];
            std::atomic<long> v(0);
            for (auto& t : threads)
            {
                t = std::thread
                (
                    [&]
                    {
                        for (int i=0 ; i < n/NUMTHREADS ; ++i)
                        {
                            ++v;
                        }
                    }
                );
            }
            for (auto& t : threads)
            {
                t.join();
            }
            //Tracer() << "v = " << v << '\n';
            cxAssert(v == n);
        });


    ceda::TimeCode("Increment std::atomic<long>", timePerTest, 10000000, 100000000, 
        [](int n) 
        { 
            std::atomic<long> v;
            for (int i=0 ; i < n ; ++i)
            {
                ++v;
            }
        });

    ceda::TimeCode("Construct/Destruct std::mutex", timePerTest, 10000000, 10000000, 
        [](int n) 
        { 
            for (int i=0 ; i < n ; ++i)
            {
                std::mutex m;
            }
        });

    ceda::TimeCode("Lock std::mutex", timePerTest, 10000000, 10000000, 
        [](int n) 
        { 
            std::mutex m;
            for (int i=0 ; i < n ; ++i)
            {
                std::lock_guard<std::mutex> lock(m);
            }
        });

    ceda::TimeCode("Construct/Destruct ManualResetEvent", timePerTest, 5000000, 10000000, 
        [](int n) 
        { 
            for (int i=0 ; i < n ; ++i)
            {
                ceda::ManualResetEvent e;
            }
        });

    ceda::TimeCode("Signal(),Wait(),Reset() on ManualResetEvent", timePerTest, 1000000, 10000000, 
        [](int n) 
        { 
            ceda::ManualResetEvent e;
            for (int i=0 ; i < n ; ++i)
            {
                e.Signal();
                e.Wait();
                e.Reset();
            }
        });

    ceda::TimeCode("Read and write thread_local int*", timePerTest, 100000000, 1000000000, 
        [](int n) 
        { 
            static thread_local int* tls = nullptr;
            [[maybe_unused]] volatile int k;
            tls = new int;
            for (int i=0 ; i < n ; ++i)
            {
                k = *tls;       // read tls
                *tls = i;       // write tls
            }
        });
}