Search code examples
templatesparallel-processingdelegatestaskd

What's the proper way to have a Task that calls an arbitrary function with a known, specific return type?


I have a value which is expensive to calculate and can be asked for ahead of time--something like a lazily initiated value whose initialization is actually done at the moment of definition, but in a different thread. My immediate thought was to use parallelism.-Task seems purpose-built for this exact use-case. So, let's put it in a class:

class Foo
{
    import std.parallelism : Task,task;
    static int calculate(int a, int b)
    {
        return a+b;
    }
    private Task!(calculate,int,int)* ourTask;
    private int _val;
    int val()
    {
        return ourTask.workForce();
    }
    this(int a, int b)
    {
        ourTask = task!calculate(a,b);
    }
}

That seems all well and good... except when I want the task to be based on a non-static method, in which case I want to make the task a delegate, in which case I start having to do stuff like this:

private typeof(task(&classFunc)) working;

And then, as it turns out, typeof(task(&classFunc)), when it's asked for outside of a function body, is actually Task!(run,ReturnType!classFunc function(Parameters!classFunc))*, which you may notice is not the type actually returned by runtime function calls of that. That would be Task!(run,ReturnType!classFunc delegate(Parameters!classFunc))*, which requires me to cast to typeof(working) when I actually call task(&classFunc). This is all extremely hackish feeling.

This was my attempt at a general template solution:

/** 
    Provides a transparent wrapper that allows for lazy
    setting of variables. When lazySet!!func(args) is called
    on the value, the function will be called in a new thread;
    as soon as the value's access is attempted, it'll return the
    result of the task, blocking if it's not done calculating.

    Accessing the value is as simple as using it like the
    type it's templated for--see the unit test.
*/
shared struct LazySet(T)
{

    /// You can set the value directly, as normal--this throws away the current task.
    void opAssign(T n)
    {
        import core.atomic : atomicStore;
        working = false;
        atomicStore(_val,n);
    }

    import std.traits : ReturnType;
/** 
    Called the same way as std.parallelism.task;
    after this is called, the next attempt to access
    the value will result in the value being set from
    the result of the given function before it's returned.
    If the task isn't done, it'll wait on the task to be done
    once accessed, using workForce.
*/
    void lazySet(alias func,Args...)(Args args)
        if(is(ReturnType!func == T))
    {
        import std.parallelism : task,taskPool;
        auto t = task!func(args);
        taskPool.put(t);
        curTask = (() => t.workForce);
        working = true;
    }
    /// ditto
    void lazySet(F,Args...)(F fpOrDelegate, ref Args args)
        if(is(ReturnType!F == T))
    {
        import std.parallelism : task,taskPool;
        auto t = task(fpOrDelegate,args);
        taskPool.put(t);
        curTask = (() => t.workForce);
        working = true;
    }

    private:
        T _val;
        T delegate() curTask;
        bool working = false;

        T val()
        {
            import core.atomic : atomicStore,atomicLoad;
            if(working)
            {
                atomicStore(_val,curTask());
                working = false;
            }
            return atomicLoad(_val);
        }
    // alias this is inherently public
    alias val this;
}

This lets me call lazySet using any function, function pointer or delegate that returns T, and then it'll calculate the value in parallel and return it, fully calculated, next time anything tries to access the underlying value, exactly as I wanted. Unit tests I wrote to describe its functionality pass, etc., it works perfectly.

But one thing's bothering me:

        curTask = (() => t.workForce);

Moving the Task around by way of creating a lambda on-the-spot that happens to have the Task in its context still seems like I'm trying to "pull one over" on the language, even if it's less "hackish-feeling" than all the casting from earlier.

Am I missing some obvious language feature that would allow me to do this more "elegantly"?


Solution

  • Templates that take an alias function parameter (such as the Task family) are finicky regarding their actual type, as they can receive any type of function as parameter (including in-place delegates that get inferred themselves). As the actual function that gets called is part of the type itself, you would have to pass it to your custom struct to be able to save the Task directly.

    As for the legitimacy of your solution, there is nothing wrong with storing lambdas to interact with complicated (or "hidden") types later.

    An alternative is to store a pointer to &t.workForce directly.

    Also, in your T val() two threads could enter if(working) at the same time, but I guess due to the atomic store it wouldn't really break anything - anyway, that could be fixed by core.atomic.cas.