18 \$cache functions using DGKeyedAsyncIndepNode
$cache functions with input parameters involve a cache map keyed by the input parameters. In effect there is a async dependent variable of the function return type for each input.
Typically a heap allocated IObject that is deleted by the CSpace GC is used when it has async DGS nodes, for example:
@import "Ceda/cxObject/DGAsyncNode.h"
$struct X isa ceda::IObject
{
$cache <<async>> int y(int a, int b) const
{
return a+b;
}
};
Xcpp generates the following C++ code:
struct X : public ceda::IObjectBaseMixin<X,ceda::EmptyBase>
{
typedef ceda::IObjectBaseMixin<X,ceda::EmptyBase> BaseClass;
int _calc_y(int a,int b) const
{
return a+b;
}
struct _depnode_y : public ceda::DGKeyedAsyncIndepNode<_depnode_y,X,std::pair<int,int>,int>
{
typedef ceda::DGKeyedAsyncIndepNode<_depnode_y,X,std::pair<int,int>,int> base;
using typename base::key_type;
using typename base::output_type;
using typename base::map_type;
static void CalcOutput(const X* _self, output_type& _output, const key_type& _key)
{
_output = _self->_calc_y(_key.first,_key.second);
}
virtual ceda::xstring Name() const
{
std::pair<int,int> const& _key = _GetKey();
return cxMakeString("X::y(" << _key.first << ',' << _key.second << ')');
}
map_type& _GetMap() const { return _self->_map_y; }
X const* _GetFinalSelf() const { return _self; }
};
mutable typename _depnode_y::map_type _map_y;
int const& y(int a,int b) const
{
return _map_y[std::make_pair(a,b)].read(this);
}
void EvictDgsNodes() const
{
BaseClass::EvictDgsNodes();
ceda::TryEvict(_map_y);
}
};
There is a nested struct named _depnode_y which represents an asynchronously calculated variable.
_depnode_y is a subclass of ceda::DGKeyedAsyncIndepNode which is defined in DGAsyncNode.h
DGKeyedAsyncIndepNode
template <typename FinalClass, typename Self, typename Key, typename Output>
class DGKeyedAsyncIndepNode : public DGIndepNode
{
public:
typedef Key key_type;
typedef Output output_type;
typedef std::map<key_type,FinalClass> map_type;
DGKeyedAsyncIndepNode() :
_self(nullptr)
{
}
virtual ssize_t ByteSize() const
{
const ssize_t MapElementSize = sizeof(typename map_type::value_type);
return StdMapElementOverhead + MapElementSize + CacheValueAdditionalSize(_output);
}
key_type const& _GetKey() const
{
return GetKeyFromValueInPair<key_type,FinalClass>(static_cast<const FinalClass*>(this));
}
virtual void VisitContainingObject(IObjectVisitor& _v) const
{
_v << _output << _self;
}
virtual void OnEvict() const
{
OnEvictCacheValue(_output);
auto& m = static_cast<const FinalClass*>(this)->_GetMap();
cxVerify(m.erase(_GetKey()) == 1);
}
virtual bool IsEvictable() const
{
return !Enabled(DF_ASYNC_RUNNING) && CacheValueIsEvictable(_output);
}
const output_type& read(Self const* self) const
{
if (_self)
{
if (Enabled(DF_ASYNC_RUNNING))
{
// read() has already been called, and an async output is currently being calculated.
// Attach out-nodes so when the output is updated the out-nodes will be invalidated
ReadBarrier();
}
else
{
// Calls to read() don't invoke the read barrier once the output has been
// calculated, because it cannot change once it is calculated.
}
}
else
{
// read() has been called for the first time.
// Attach out-nodes - these will be invalidated when the async output has been calculated.
ReadBarrier();
_self = self;
CSpace* cspace = GetThreadPtr<CSpace>();
cxAssert(!Enabled(DF_ASYNC_RUNNING));
SetFlag(DF_ASYNC_RUNNING);
// _output hasn't been calculated before so post a task to calculate it.
PostAsyncTask(cspace, [this]()
{
PrepareThreadLocalStorage(_self);
// Calculate output from key without a CSpace lock, hold output in a local variable
output_type localOutput;
FinalClass::CalcOutput(_self, localOutput, _GetKey());
{
// Use CSpace lock to swap in the output, then invalidate the out-nodes
CSpaceTxn txn;
cxAssert(Enabled(DF_ASYNC_RUNNING));
UpdateOutput(_output, localOutput);
OnChange();
ClearFlag(DF_ASYNC_RUNNING);
if (TypeHasAdditionalSize<output_type>()) UpdateByteSize();
}
});
}
return _output;
}
protected:
mutable Self const* _self;
mutable output_type _output; // Protected by a CSpace lock
};
DGKeyedAsyncIndepNode::OnEvict() causes the DGKeyedAsyncIndepNode node to be deleted from the std::map when it is evicted by the DGS.
Making sure we don't delete objects running async tasks
The CSpace GC visits the DGSystem object which visits the eviction queue which in turn calls the virtual function VisitContainingObject on every independent and dependent node in the eviction queue.
DGKeyedAsyncIndepNode::VisitContainingObject() visits the member self_ which is a pointer to the containing X. Therefore we conclude that while an object of type X has any map elements in the eviction queue then it cannot be GC'd.
DGS nodes which have posted an async task are always in the eviction queue.
The DF_ASYNC_RUNNING bit of an DGKeyedAsyncIndepNode object is set in a call to read() when the CSpace is locked and before the async task is posted. The DF_ASYNC_RUNNING bit is only cleared during a CSpace lock by the async task. If DF_ASYNC_RUNNING is set then IsEvictable() returns false. Therefore an DGKeyedAsyncIndepNode object cannot be evicted while the async task is running.
DGAsyncNode cannot have its own task executer
The intention is for a class derived from DGAsyncNode to be the mapped value of a std::map
struct Node : DGAsyncNode<...>
{
...
};
mutable std::map<Key,Node> _map;
std::map isn't threadsafe, so all access to the map must be protected by the CSpaceLock. In particular, deleting a map entry must be done in a CSpaceLock. Therefore a DGAsyncNode destructs in a CSpaceLock. Therefore a DGAsyncNode cannot have its own task executer which is closed when it destructs (to avoid dead-lock scenarios)
Instead the CSpace has a task executer. The CSpace can close the task executer without a CSpaceLock when the CSpace is being destroyed