23 CacheMap

CacheMap<Key,Value,Calculator> implements a cache that maps from Key to Value. It is assumed the Value can be calculated from the Key with an externally provided "calculator" function.

Saying that the value is "calculated" from the key should not be taken too literally. For example, it may correspond to loading an object from disk where the key represents some kind of object identifier.

Clients retrieve values for given key values with calls to GetValue(). This method is threadsafe. The implementation ensures that at most one thread calculates a value for a given key. I.e. when a key is not found in the map the first calling thread is given responsibility for calculating the value and subsequent threads block in an efficient wait state until the value is calculated.

Note that the calculator must be threadsafe - i.e. it must itself allow for concurrent calculation of values for different keys.

The calculator must implement operator() that takes the key as a single argument (either by value or reference) and returns the calculated value.

struct MyCalculator
{
    Value operator()(const Key& key);
};

It is permissible for operator() to throw an exception! This exception will be passed out to the thread that called GetValue() on the CacheMap. Queued threads will subsequently get their own chance to try to calculate the value (and of course they may throw exceptions as well).

The calculator is stored as a member of the CacheMap, and may be provided in the constructor.

The CacheMap doesn't implement any policy for removing entries. Clear() and RemoveCachedValue() are functions provided to support removal of map entries.

Usage in CEDA

CacheMap was used to implement the Resident Object Table (ROT) of a PSpace. However that is no longer done because the ROT requirements became more complex in order to support asynchronous binding to objects from the LSS, which represent independent nodes of the DGS.

CacheMap.h

// CacheMap.h
//
// Author David Barrett-Lennard
// (C)opyright Cedanet Pty Ltd 2006

#pragma once
#ifndef Ceda_cxThread_CacheMap_H
#define Ceda_cxThread_CacheMap_H

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

namespace ceda
{
template <typename Key, typename Value, typename Calculator>
class CacheMap
{
public:
    CacheMap(Calculator c = Calculator()) : calculator_(c) {}

    void Clear()
    {
        std::lock_guard<std::mutex> lock(_mutex);
        map_.clear();
    }

    bool RemoveCachedValue(const Key& key)
    {
        std::lock_guard<std::mutex> lock(_mutex);
        ssize_t numRemoved = map_.erase(key);
        cxAssert(numRemoved == 0 || numRemoved == 1);
        return numRemoved == 1;
    }

    // Used to poll for whether an entry for the given key is currently present in the map.
    bool IsCached(const Key& key) const
    {
        std::lock_guard<std::mutex> lock(_mutex);
        return map_.find(key) != map_.end();
    }

    // Allows for polling whether an entry in the map is already present.  This function always
    // returns quickly without blocking on a calculation - either directly or indirectly.
    bool GetCachedValue(const Key& key, Value& v) const
    {
        std::lock_guard<std::mutex> lock(_mutex);
        typename MAP::const_iterator i = map_.find(key);
        if (i != map_.end())
        {
            v = i->second;
            return true;
        }
        else
        {
            return false;
        }
    }

    // Try to insert a (key,value) pair.  Returns false if insertion failed because that key
    // is already present.
    bool InsertCachedValue(const Key& key, const Value& value)
    {
        std::lock_guard<std::mutex> lock(_mutex);
        std::pair<typename MAP::iterator,bool> r = map_.insert(MAP::value_type(key,value));
        return r.second;
    }

    void SetCachedValue(const Key& key, const Value& value)
    {
        std::lock_guard<std::mutex> lock(_mutex);
        map_[key] = value;
    }
    
    Value GetValue(const Key& key)
    {
retry:
        EventEntry* e = nullptr;
        bool needToCalc = false;
        {
            std::lock_guard<std::mutex> lock(_mutex);

            typename MAP::const_iterator i = map_.find(key);
            if (i != map_.end())
            {
                // Value is cached in the map
                return i->second;
            }
            else
            {
                typename EVENTMAP::iterator j = eventMap_.find(key);
                if (j == eventMap_.end())
                {
                    // This thread is responsible for calculating the value
                    needToCalc = true;
                    e = &eventMap_[key];
                    cxAssert(e->status_ == NOT_CALCULATED);
                    e->status_ = CALCULATING;
                }
                else
                {
                    // Another thread has already taken responsibility for calculating the value
                    e = &j->second;

                    if (e->status_ == NOT_CALCULATED)
                    {
                        // The thread that had tried to calculate the value must have thrown an 
                        // exception.  Therefore make this thread responsible for calculating the
                        // value
                        needToCalc = true;
                        e->status_ = CALCULATING;
                        cxAssert(e->count_ > 0);
                        cxAssert(e->event_);
                        e->event_->Reset();
                    }
                    else if (e->status_ == CALCULATED)
                    {
                        // This is only possible if entries have been removed from the main map
                        // or AllowInsertionInMap() can return false.
                        return e->value_;
                    }
                    else
                    {
                        cxAssert(e->status_ == CALCULATING);
                        if (e->count_++ == 0)
                        {
                            cxAssert( e->event_ == nullptr );
                            e->event_ = new ManualResetEvent;
                        }
                    }
                    cxAssert( e->event_ );
                }
            }
        }

        cxAssert(e);
        if (needToCalc)
        {
            Value v;

            try
            {
                cxAssert(e->status_ == CALCULATING);
                v = calculator_(key);
            }
            catch(...)              // todo: find a better solution, catching with ... and rethrowing stops the visual studio debugger stopping at the point where an access violation occurred etc
            {
                std::lock_guard<std::mutex> lock(_mutex);
                if (e->count_ == 0)
                {
                    cxAssert( e->event_ == nullptr );
                    eventMap_.erase(key);
                }
                else
                {
                    // Calculation failed.  Need to indicate failure to other threads and signal
                    // them
                    cxAssert( e->event_ );
                    e->status_ = NOT_CALCULATED;
                    e->event_->Signal();
                }
                throw;
            }

            std::lock_guard<std::mutex> lock(_mutex);
            
            if (calculator_.AllowInsertionInMap(v))
            {
                map_[key] = v;
            }
            
            if (e->count_ == 0)
            {
                cxAssert( e->event_ == nullptr );
                eventMap_.erase(key);
            }
            else
            {
                cxAssert( e->event_ );
                e->value_ = v;
                e->status_ = CALCULATED;
                e->event_->Signal();
            }

            return v;
        }
        else
        {
            cxAssert( e->event_ );
            e->event_->Wait();

            std::lock_guard<std::mutex> lock(_mutex);

            if (e->status_ == CALCULATED)
            {
                Value v = e->value_;
                if (--e->count_ == 0)
                {
                    delete e->event_;
                    eventMap_.erase(key);
                }
                return v;
            }
            else
            {
                if (--e->count_ == 0)
                {
                    delete e->event_;
                    e->event_ = nullptr;
                    if (e->status_ == NOT_CALCULATED)
                    {
                        eventMap_.erase(key);
                    }
                }
                goto retry;
            }
        }
    }
    
    void Transfer(CacheMap& src)
    {
        std::lock_guard<std::mutex> lock1(_mutex);
        std::lock_guard<std::mutex> lock2(src._mutex);
        
        cxAssert(src.eventMap_.empty());
        
        for (typename MAP::iterator i = src.map_.begin() ; i != src.map_.end() ; ++i)
        {
            cxAssert(map_.find(i->first) == map_.end());
            map_[i->first] = i->second;
        }
        src.map_.clear();
    }
    
    Calculator& GetCalculator() { return calculator_; }

protected:
    mutable std::mutex _mutex;

    Calculator calculator_;
    
    typedef std::map<Key,Value> MAP;
    MAP map_;

    enum EStatus
    {
        NOT_CALCULATED,
        CALCULATING,
        CALCULATED
    };
    
    struct EventEntry
    {
        EventEntry() : status_(NOT_CALCULATED), count_(0), event_(nullptr) {}
    
        EStatus status_;
        int count_;
        ManualResetEvent* event_;
        Value value_;
    };
    friend struct EventEntry;
    typedef std::map<Key, EventEntry> EVENTMAP;
    EVENTMAP eventMap_;
};

} // namespace ceda

#endif // include guard

CacheMapTests.cpp in txThread

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

#ifdef _MSC_VER
    // Identifier was truncated to 'number' characters in the debug information
    #pragma warning(disable: 4786)  
#endif

#include "Ceda/cxThread/CacheMap.h"
#include "Ceda/cxUtils/Tracer.h"
#include "Ceda/cxUtils/IsaacRandom.h"
#include "Ceda/cxUtils/TestTimer.h"
#include <thread>
#include <mutex>
#include <algorithm>

///////////////////////////////////////////////////////////////////////////////////////////////////
// Timing test

struct IncrementCalculator
{
    IncrementCalculator() : numCalcs_(0) {}
    
    int operator()(int x)
    {
        ++numCalcs_;
        return x+1;
    }
    static bool AllowInsertionInMap(int) { return true; }
    
    int numCalcs_;
};

typedef ceda::CacheMap<int,int,IncrementCalculator> IncrementCacheMap;

/*
Performance on Pentium IV 2GHz
    
  numIterations    numKeys         Time (ns)
    1000000           1            200  
    1000000           10           250  
    1000000           100          280  
    1000000           1000         340  
    1000000           10000        480  
    1000000           100000       2150 
    1000000           1000000      6300 
    1000000           10000000     9000 
    1000000           100000000    9500 

Performance on Intel Core 2 Duo E8400 3GHz
    
  numIterations    numKeys         Time (ns)
    1000000           1            57 
    1000000           10           72 
    1000000           100          96 
    1000000           1000         112 
    1000000           10000        172 
    1000000           100000       280 
    1000000           1000000      890 
    1000000           10000000     1270 
    1000000           100000000    1440 
*/

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

    int NK = 9;
    double timePerTest = timeForTestInSecs/NK;
    
    // Tests with numKeys = 1,10,100,...,100M
    int numKeys = 1;
    for (int i=0 ; i < NK ; ++i)
    {
        bool seedFromClock = true;
        ceda::IsaacRandomNumberGen isaac(seedFromClock);

        double rate = 300000.0;
        if (numKeys > 500) rate = 50000;

        ceda::TimeCode( cxMakeString2("GetValue() with random keys on IncrementCacheMap with " << numKeys << " keys"), timePerTest, rate, 10000000, 
            [numKeys, &isaac](int n) 
            { 
                IncrementCacheMap cm;
                for (int i = 0 ; i < n ; ++i)
                {
                    int r = isaac.GetUniformDistInteger(0,numKeys);
                    cxAlwaysAssert(cm.GetValue(r) == r+1);
                }
                Tracer() << "    Num calc = " << cm.GetCalculator().numCalcs_ << '\n';
            });

        numKeys *= 10;
    }
}

///////////////////////////////////////////////////////////////////////////////////////////////////
// Unit test

class AbortedException {};

int volatile g_v = 0;

int g_counter = 0;

struct TestCalculator
{
    TestCalculator() {}
    TestCalculator(const TestCalculator& rhs) : isaac_(rhs.isaac_) {}
    
    int operator()(int x)
    {
        for (int i=0 ; i < 100 ; ++i)
        {
            ++g_v;
        }

        //Sleep(1);
        {
            std::lock_guard<std::mutex> lock(mutex_);
            if (isaac_.GetUniformDistInteger(0,10) == 0)
            {
                //Tracer() << "Throwing exception\n";
                throw AbortedException();
            }
        }

        //Sleep(1);
        ++g_counter;
        return x+1;
    }
    static bool AllowInsertionInMap(int) { return true; }

    std::mutex mutex_;
    ceda::IsaacRandomNumberGen isaac_;
};

typedef ceda::CacheMap<int,int,TestCalculator> DemoCacheMap;

///////////////////////////////////////////////////////////////////////////////////////////////////
// CacheMapThread

const bool SEED_FROM_CLOCK = true;
 
class CacheMapThread
{
public:
    CacheMapThread(DemoCacheMap& cm, int numKeys, bool trace, int indent) : 
        cm_(cm),
        isaac_(SEED_FROM_CLOCK),
        requestShutDown_(false),
        numGets_(0),
        numFails_(0),
        //numKeys_(numKeys),
        trace_(trace),
        indent_(indent)
    {
        thread_ = std::thread( &CacheMapThread::WorkerThreadFn, this );
    }
    ~CacheMapThread()
    {
        if (trace_)
        {
            Tracer() << "Stopping CacheMapThread : numGets_ = " << numGets_ << "  numFails_ = " << numFails_ << '\n';
        }
        requestShutDown_ = true;
        thread_.join();                  // Blocks until worker thread exits
    }

    void WorkerThreadFn();

private:
    DemoCacheMap& cm_;
    ceda::IsaacRandomNumberGen isaac_;
    std::thread thread_;
    volatile bool requestShutDown_;
    int numGets_;
    int numFails_;
    //int numKeys_;
    bool trace_;
    int indent_;
};

void CacheMapThread::WorkerThreadFn()
{
    ceda::TraceIndenter id(indent_);

    //Tracer() << "CacheMapThread " << GetCurrentThreadId() << '\n';

    while(!requestShutDown_)
    {
        if (isaac_.GetUniformDistInteger(0,10) == 0)
        {
            Sleep(1);
        }
        ++numGets_;
        if (isaac_.GetUniformDistInteger(0,5) == 0)
        {
            cm_.RemoveCachedValue( g_counter + isaac_.GetUniformDistInteger(-4,4) );
        }
        int r = g_counter + isaac_.GetUniformDistInteger(-4,4);
        try
        {
            cxAlwaysAssert(cm_.GetValue(r) == r+1);
        }
        catch(AbortedException&)
        {
            ++numFails_;
            //Tracer() << "Caught abort exception\n";
        }
    }
}

void UnitTestCacheMap2(double timeForTestInSecs,int numKeys,bool trace)
{
    ceda::HPTimer timer;

    g_counter = 0;
    DemoCacheMap cm;

    CacheMapThread t1(cm, numKeys, trace, 20);
    Sleep(5);
    CacheMapThread t2(cm, numKeys, trace, 40);
    Sleep(5);
    CacheMapThread t3(cm, numKeys, trace, 60);
    Sleep(5);
    CacheMapThread t4(cm, numKeys, trace, 80);
    Sleep(5);
    CacheMapThread t5(cm, numKeys, trace, 100);
    Sleep(5);
    CacheMapThread t6(cm, numKeys, trace, 120);
    Sleep(5);

    for (int i=0 ; i < (int) (timeForTestInSecs*10) ; ++i)
    {
        if (i % 10 == 0) ceda::xcout << "Time: " << i/10.0 << '\n' << ceda::flush;
        Sleep(100);
    }

    timer.Done() << "numKeys= " << numKeys << " calls to CalcValue = " << g_counter;
}

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

void UnitTestCacheMap(double timeForTestInSecs)
{
    Tracer() << "Unit Test CacheMap\n";
    ceda::TraceIndenter indent(4);

    int NK = 9;
    int numExperiments = 5;
    if (timeForTestInSecs < 10)
    {
        numExperiments = 1;
        NK = 4;
    }

    double timePerTest = timeForTestInSecs/(NK*numExperiments);
    Tracer() << "timePerTest = " << timePerTest << '\n';
    
    // Tests with numKeys = 1,10,100,...,100M
    int numKeys = 1;
    for (int i=0 ; i < NK ; ++i)
    {
        for (int c = 0; c < numExperiments ; ++c)
        {
            UnitTestCacheMap2(timePerTest,numKeys, i==0);
        }
        numKeys *= 10;
    }
}