CSpaceObjectEviction.cpp

// CSpaceObjectEviction.cpp
//
// Author Jesse Pepper
// (C)opyright Cedanet Pty Ltd 2007

@import "Ceda/cxObject/Object.h"
@import "Ceda/cxObject/WCSpace.h"
@import "Ceda/cxObject/IObjectVisitor.h"
@import "ExampleUtils.h"
#include "Ceda/cxUtils/HPTime.h"
#include <assert.h>
#include <thread>
#include <mutex>

/*
Object Eviction
---------------

This example demonstrates the way IObject eviction works in Ceda.  A CacheDemo object is created
which contains a mutable ptr<Ix>.  This is a ptr to a recalculatable cache object of type X.

CacheDemo's Get() method needs to determine if the cache is dirty, and if so, recalculate it.
A dirty read is signified by the ReadEvictablePtr() function returning a null ptr.

In order to avoid a obtaining a critical section lock when reading the cached value each time,
the double-checked locking idiom is utilised.  This gets a critical section lock only when the
cache is dirty, and then with the lock checks again if it is dirty.  Only then is the value
recalculated.  This ensures that two threads don't both recalculate the cache, while still
allowing high-speed retrivals of the cached value (which is probably very common).

Two worker threads are created that each attempt to read the cached state every five seconds, 
while the garbage collector should be running every second and evict the cache.  If the cache
gets accessed (SetTouched is called on the pointer) between garbage collections the cached
object is not evicted.  This can be demonstrated by decreasing the worker threads' sleep time
to 10ms.
*/
namespace CSpaceObjectEviction
{
    $interface+ Ix : ceda::IObject
    {
        int32 x;
    };
    
    $struct X isa Ix
    {
        $int32 x;
    };

    $class CacheDemo isa ceda::IObject 
    {
        cxNotCloneable(CacheDemo)
        
    public:
        CacheDemo() {}
            
        void VisitObjects(ceda::IObjectVisitor& v) const
        {
            // Evictable pointers must be visited in this way
            v.VisitEvictableObject((ceda::ptr<const ceda::IObject>&) m_cache);
        }
        
        ceda::ptr<Ix> Get() const
        {
            // Evictable pointers can't be dereferenced directly, they must be retrieved into a frame 
            // variable (here p) using the ReadEvictablePtr() function, then p can be dereferenced.
            ceda::ptr<Ix> p;
            if (!ReadEvictablePtr(m_cache,p))
            {
                // The double-checked locking idiom
                std::lock_guard<std::mutex> lock(mutex_);
                if (!ReadEvictablePtr(m_cache,p))
                {
                    Tracer() << "recalculate X\n";
                    // This news an X (the cached object) and registers it with the CSpace (and its GC).
                    p = $new X;
                    
                    // tell the framework the effective size of the cached member in bytes, for this 
                    // example to work, the evictable size must be large enough so that one instance
                    // of the object in memory exceeds the EvictionThreshold (1MB by default)
                    SetEvictableObjectSizeInBytes(p,1024*1024*3);
                    
                    // calculate the cached state, for the purposes of this example we simply set the value
                    // of the integer, this could be a time consuming calculation.
                    p->x = 1071;
                    SetEvictablePtr(m_cache,p);
                }
            }
            
            // Objects are evicted on an LRU basis. SetTouched(p) must be called from within a lock. 
            // It ensures p is held in memory for the life of the lock, and marks p as being the most 
            // recently used.
            SetTouched(p);
            return p;
        }

    private:
        mutable std::mutex mutex_;
        mutable ceda::ptr<Ix> m_cache;
    };
    
    // When XAccessor objects are created, they start a worker thread that repeatedly gets a lock
    // and then access the shared CacheDemo object by calling it's Get() method.
    class XAccessor
    {
    public:
        XAccessor(CacheDemo& cd, ceda::CSpace* cspace) : requestShutDown_(false), m_cd(cd), m_cspace(cspace)
        {
            thread_ = std::thread( &XAccessor::WorkerThreadFn, this );
        }
        
        ~XAccessor()
        {
            requestShutDown_ = true;
            thread_.join();                  // Blocks until worker thread exits
        }
        
        void WorkerThreadFn()
        {
            ceda::SetThreadPtr<ceda::CSpace>(m_cspace);
            
            for (int i=0 ; !requestShutDown_ ; ++i)
            {
                {
                    ceda::CSpaceLock lock(m_cspace,ceda::ECSpaceLockMode::SharedRead);
                    int x = m_cd.Get()->x;
                    Tracer() << "time = " << i << ", x = " << x << '\n';
                }
                Sleep(500);
            }
        }

    private:        
        std::thread thread_;
        volatile bool requestShutDown_;
        CacheDemo& m_cd;
        ceda::CSpace* m_cspace;
    };
    
    void Run()
    {
        ceda::TraceGroup g("CSpaceObjectEviction");
        
        ceda::CSpaceCreator cspace(100);    // GC every 100 msec
        
        {
            CacheDemo* cd = new CacheDemo;
            {
                ceda::CSpaceLock lock(cspace);
                ceda::RegisterGcObject(cd);
                Tracer() << "Adding root cd\n";
                ceda::AddGcRoot(cd);
            }
            
            XAccessor xa1(*cd, ceda::GetThreadPtr<ceda::CSpace>());
            XAccessor xa2(*cd, ceda::GetThreadPtr<ceda::CSpace>());
            Tracer() << "Main thread sleeping\n";
            Sleep(2000);
        }
    }
}