【问题标题】:Tasks in Recursion递归中的任务
【发布时间】:2014-05-04 16:43:49
【问题描述】:

我有一个递归遍历二叉树的函数。由于这些操作是计算密集型的,我想使用以下任务在递归函数中生成多个线程:

static void Traverse<T>(Tree<T> node, Action<T> action) 
{ 
 if (node == null) return; 
 var t1 = Task.Factory.StartNew(() => action(node.Data)); 
 var t2 = Task.Factory.StartNew(() => Traverse(node.Left, action)); 
 var t3 = Task.Factory.StartNew(() => Traverse(node.Right, action)); 
 Task.WaitAll(t1, t2, t3); 
} 

现在这似乎确实有效。但是我想知道在以递归方式使用任务时是否需要注意什么。例如,如果树的深度很长,它是否无法以某种方式为较低级别创建任务并等待其他任务完成(这些任务永远不会完成,因为它们正在等待较低级别的任务完成)?

【问题讨论】:

  • 您的代码出错,Traverse 必须是Action&lt;Tree&lt;T&gt;&gt;,您的代码不会按原样编译,因为您将Tree&lt;T&gt; 传递给action(node) 但它只需要T
  • 感谢 Scott 指出。更正了代码。我做了一些更改以简化问题并引入了错误。

标签: c# multithreading recursion .net-4.0 task-parallel-library


【解决方案1】:

如果树非常大,许多任务可能会导致问题完全耗尽整个线程池,从而导致其他地方出现性能问题,这是因为节点与其父节点之间没有依赖关系,因此所有节点都将尝试并发运行。我要做的是让你的Tree&lt;T&gt; 类实现IEnumerable&lt;T&gt;,它将返回它自己的Data 属性和所有它的孩子的Data 属性,然后使用Parallel.ForEach

static void Traverse<T>(Tree<T> node, Action<T> action) 
{
    Parallel.ForEach(node, action);
}


//Elsewhere
class Tree<T> : IEnumerable<T>
{
    Tree<T> Left { get; set; }
    Tree<T> Right { get; set; } 
    T Data { get; set; }

    public IEnumerator<T> GetEnumerator()
    {
        yield return this.Data;

        if (Left != null)
        {
            foreach (var left in Left)
            {
                yield return left.Data;
            }
        }

        if (Right != null)
        {
            foreach (var right in Right)
            {
                yield return right.Data;
            }
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

您需要关心的唯一“问题”是树中是否存在任何闭合循环,其中子节点可能是更高级别节点的父节点,这会导致无限递归。


编辑:这是一个新版本,它不在 GetEnumerator 上使用递归,而是使用 Stack&lt;Tree&lt;T&gt;&gt; 对象来保存状态,所以如果你有 高大的树木你不能拥有StackOverflowException。此外,如果您从注释行中删除 cmets,它将停止以前版本的“无限递归”问题。但是如果你知道你不会有任何循环结构,那就没有必要了,所以我把它注释掉了。

class Tree<T> : IEnumerable<T>
{
    Tree<T> Left { get; set; }
    Tree<T> Right { get; set; }
    T Data { get; set; }

    public IEnumerator<T> GetEnumerator()
    {
        Stack<Tree<T>> items = new Stack<Tree<T>>();
        //HashSet<Tree<T>> recursiveCheck = new HashSet<Tree<T>>();

        items.Push(this);
        //recursiveCheck.Add(this);

        while (items.Count > 0)
        {
            var current = items.Pop();

            yield return current.Data;

            if (current.Left != null)
                //if(recursiveCheck.Add(current.Left))
                    items.Push(current.Left);
            if (current.Right != null)
                //if (recursiveCheck.Add(current.Right))
                    items.Push(current.Right);
        }

    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

【讨论】:

  • 谢谢斯科特。使用枚举器确实有意义。
  • “完全耗尽整个线程池”任务内联难道不能避免这个问题吗?
  • @svick 我不知道,可能。
  • @svick 你说得有道理。我测试了最大并发为 2 和深度为 20 的树,我可以清楚地看到它在父线程中内联的多个任务使用相同的线程。尽管如此,我仍然觉得我应该采用 Scott 建议的枚举器方法,以避免使用递归可能会忽略的任何其他问题。
【解决方案2】:

就像你说的那样,递归地产生线程似乎不是一个好主意,如果你的树足够长,你最终会得到很多线程,因为会有很多开销,所以会变慢,或者你' 最终会达到程序中并行线程的限制。所以我建议你改用 ThreadPool 来管理你的线程。

您可能有一个线程来导航树,另外两个线程来完成繁重的工作。您还应该注意,除非您有一些阻塞操作,如 I/O 读/写或一些网络正在进行,否则使用线程不会很好。如果你不这样做,最好只使用一个线程来完成繁重的工作,而另一个线程来遍历树。

【讨论】:

  • 感谢您的回答,但我认为 Task.StartNew() 使用线程池线程
【解决方案3】:

我不认为它会在任何时候停止工作,但使用多线程会增加 CPU 使用率,因为计算机同时执行更多操作,因此不使用多线程和不使用多线程可能更安全,但速度更慢只需使用以下内容:

static void Traverse<T>(Tree<T> node, Action<T> action)
{
 if (node == null) return;
 action(node);
 Traverse(node.Left, action);
 Traverse(node.Right, action);
}

这会比较慢,所以如果您担心它的运行速度有多快,您会想要使用您的原始版本。

【讨论】:

  • “更安全”是什么意思? 使用所有可用的 CPU 是一件好事吗?
  • 如果你使用更多的 CPU,它可能会导致整个计算机出现极度延迟,并且如果树非常大,也会导致硬盘开始过热,但只有当它变得非常 CPU 密集型。
  • 这没有任何意义。当您有一个非常内存密集型的应用程序时,您描述的延迟可能会发生,而仅使用大量 CPU 的应用程序不会发生这种情况(假设该应用程序没有高优先级) .而且 CPU 使用率当然与硬盘无关。
猜你喜欢
  • 2016-05-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多