如何使用C#异步/等待作为独立的CPS转换

Sam*_*dal 9 c# stack-overflow combinators continuation-passing async-await

注1:这里的CPS代表“持续通过风格”

我对了解如何挂接到C#异步机制非常感兴趣。基本上,据我了解的C#异步/等待功能,编译器将执行CPS转换,然后将转换后的代码传递给上下文对象,该对象管理各个线程上的任务调度。

您是否认为可以利用该编译器功能来创建功能强大的组合器,同时又保留默认的线程方面?

一个例子就是可以递归和记忆诸如

async MyTask<BigInteger> Fib(int n)     // hypothetical example
{
    if (n <= 1) return n;
    return await Fib(n-1) + await Fib(n-2);
}
Run Code Online (Sandbox Code Playgroud)

我设法做到这一点:

void Fib(int n, Action<BigInteger> Ret, Action<int, Action<BigInteger>> Rec)
{
    if (n <= 1) Ret(n);
    else Rec(n-1, x => Rec(n-2, y => Ret(x + y)));
}
Run Code Online (Sandbox Code Playgroud)

(不使用异步,非常笨拙...)

或使用monadWhile<X> = Either<X, While<X>>

While<X> Fib(int n) => n <= 1 ?
    While.Return((BigInteger) n) :
    from x in Fib(n-1)
    from y in Fib(n-2)
    select x + y;
Run Code Online (Sandbox Code Playgroud)

好一点,但看起来不像异步语法:)


我在E. Lippert的博客上提出了这个问题,他很友好,让我知道这确实是可能的。


实现ZBDD库时对我的需求:(一种特殊的DAG)

  • 许多复杂的相互递归操作

  • 实际示例中堆栈不断溢出

  • 仅在完全记忆的情况下才实用

手动CPS和反递归非常繁琐且容易出错。


对我进行的酸测试(堆栈安全)将类似于:

async MyTask<BigInteger> Fib(int n, BigInteger a, BigInteger b)
{
    if (n == 0) return b;
    if (n == 1) return a;
    return await Fib(n - 1, a + b, a);
}
Run Code Online (Sandbox Code Playgroud)

Fib(10000, 1, 0)使用默认行为会产生堆栈溢出。甚至更好的是,在开始时使用带有备注的代码来进行计算Fib(10000)

Dmy*_*lov 1

这是我的解决方案版本。它是堆栈安全的,不使用线程池,但有特定的限制。特别是它需要尾递归风格的方法,因此像这样的结构Fib(n-1) + Fib(n-2)不起作用。另一方面,实际上以迭代方式执行的尾递归性质不需要记忆,因为每次迭代都被调用一次。它没有边缘情况保护,但它只是一个原型而不是最终解决方案:

public class RecursiveTask<T>
{
    private T _result;

    private Func<RecursiveTask<T>> _function;

    public T Result
    {
        get
        {
            var current = this;
            var last = current;

            do
            {
                last = current;
                current = current._function?.Invoke();
            } while (current != null);

            return last._result;
        }
    }

    private RecursiveTask(Func<RecursiveTask<T>> function)
    {
        _function = function;
    }

    private RecursiveTask(T result)
    {
        _result = result;
    }

    public static implicit operator RecursiveTask<T>(T result)
    {
        return new RecursiveTask<T>(result);
    }

    public static RecursiveTask<T> FromFunc(Func<RecursiveTask<T>> func) => new RecursiveTask<T>(func);
}
Run Code Online (Sandbox Code Playgroud)

以及用法:

class Program
{
    static RecursiveTask<int> Fib(int n, int a, int b)
    {
        if (n == 0) return a;
        if (n == 1) return b;

        return RecursiveTask<int>.FromFunc(() => Fib(n - 1, b, a + b));
    }

    static RecursiveTask<int> Factorial(int n, int a)
    {
        if (n == 0) return a;

        return RecursiveTask<int>.FromFunc(() => Factorial(n - 1, n * a));
    }


    static void Main(string[] args)
    {
        Console.WriteLine(Factorial(5, 1).Result);
        Console.WriteLine(Fib(100000, 0, 1).Result);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,重要的是返回一个包装循环调用的函数,而不是调用本身,以避免真正的递归。

更新 下面是另一个实现,它仍然不使用 CPS 变换,但允许使用接近代数递归的语义,即它支持函数内的多个类似递归的调用,并且不要求函数是尾递归的。

public class RecursiveTask<T1, T2>
{
    private readonly Func<RecursiveTask<T1, T2>, T1, T2> _func;
    private readonly Dictionary<T1, RecursiveTask<T1, T2>> _allTasks;
    private readonly List<RecursiveTask<T1, T2>> _subTasks;
    private readonly RecursiveTask<T1, T2> _rootTask;
    private T1 _arg;
    private T2 _result;
    private int _runsCount;
    private bool _isCompleted;
    private bool _isEvaluating;

    private RecursiveTask(Func<RecursiveTask<T1, T2>, T1, T2> func)
    {
        _func = func;
        _allTasks = new Dictionary<T1, RecursiveTask<T1, T2>>();
        _subTasks = new List<RecursiveTask<T1, T2>>();
        _rootTask = this;
    }

    private RecursiveTask(Func<RecursiveTask<T1, T2>, T1, T2> func, T1 arg, RecursiveTask<T1, T2> rootTask) : this(func)
    {
        _arg = arg;
        _rootTask = rootTask;
    }

    public T2 Run(T1 arg)
    {
        if (!_isEvaluating)
            BuildTasks(arg);

        if (_isEvaluating)
            return EvaluateTasks(arg);

        return default;
    }

    public static RecursiveTask<T1, T2> Create(Func<RecursiveTask<T1, T2>, T1, T2> func)
    {
        return new RecursiveTask<T1, T2>(func);
    }

    private void AddSubTask(T1 arg)
    {
        if (!_allTasks.TryGetValue(arg, out RecursiveTask<T1, T2> subTask))
        {
            subTask = new RecursiveTask<T1, T2>(_func, arg, this);
            _allTasks.Add(arg, subTask);
            _subTasks.Add(subTask);
        }
    }

    private T2 Run()
    {
        if (!_isCompleted)
        {
            var runsCount = _rootTask._runsCount;
            _result = _func(_rootTask, _arg);
            _isCompleted = runsCount == _rootTask._runsCount;
        }
        return _result;
    }

    private void BuildTasks(T1 arg)
    {
        if (_runsCount++ == 0)
            _arg = arg;

        if (EqualityComparer<T1>.Default.Equals(_arg, arg))
        {
            Run();

            var processed = 0;
            var addedTasksCount = _subTasks.Count;
            while (processed < addedTasksCount)
            {
                for (var i = processed; i < addedTasksCount; i++, processed++)
                    _subTasks[i].Run();
                addedTasksCount = _subTasks.Count;
            }
            _isEvaluating = true;
        }
        else
            AddSubTask(arg);
    }

    private T2 EvaluateTasks(T1 arg)
    {
        if (EqualityComparer<T1>.Default.Equals(_arg, arg))
        {
            foreach (var task in Enumerable.Reverse(_subTasks))
                task.Run();

            return Run();
        }
        else
        {
            if (_allTasks.TryGetValue(arg, out RecursiveTask<T1, T2> task))
                return task._isCompleted ? task._result : task.Run();
            else
                return default;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

class Program
{
    static int Fib(int num)
    {
        return RecursiveTask<int, int>.Create((t, n) =>
        {
            if (n == 0) return 0;
            if (n == 1) return 1;

            return t.Run(n - 1) + t.Run(n - 2);
        }).Run(num);
    }

    static void Main(string[] args)
    {
        Console.WriteLine(Fib(7));
        Console.WriteLine(Fib(100000));
    }
}
Run Code Online (Sandbox Code Playgroud)

作为好处,它是堆栈安全的,不使用线程池,不承担async await基础设施的负担,使用记忆并允许使用或多或少可读的语义。当前的实现意味着仅使用具有单个参数的函数。为了使其适用于更广泛的函数,应该为不同的通用参数集提供类似的实现:

RecursiveTask<T1, T2, T3>
RecursiveTask<T1, T2, T3, T4>
...
Run Code Online (Sandbox Code Playgroud)