如何使用异步/并行处理迭代执行深度优先搜索?

时间:2014-08-21 17:27:04

标签: c# .net algorithm task-parallel-library async-await

这是一个执行DFS搜索并返回给定顶级项目ID的所有项目列表的方法。我如何修改它以利用并行处理?目前,对于堆栈中的每个项目,逐个调用获取子项。如果我可以同时获取堆栈中多个项目的子项,并且更快地填充我的返回列表,那将是很好的。我怎样才能以线程安全的方式执行此操作(使用async / await或TPL或其他任何方式)?

private async Task<IList<Item>> GetItemsAsync(string topItemId)
{
    var items = new List<Item>();   
    var topItem = await GetItemAsync(topItemId);

    Stack<Item> stack = new Stack<Item>();           
    stack.Push(topItem);
    while (stack.Count > 0)
    {
        var item = stack.Pop();
        items.Add(item);                   

        var subItems = await GetSubItemsAsync(item.SubId);

        foreach (var subItem in subItems)
        {
            stack.Push(subItem);
        }
    }

    return items;   
}

编辑: 我正在考虑这些方面的事情,但它并没有聚集在一起:

var tasks = stack.Select(async item =>
{
    items.Add(item);           
    var subItems = await GetSubItemsAsync(item.SubId);

    foreach (var subItem in subItems)
    {
        stack.Push(subItem);
    }   
}).ToList();

if (tasks.Any())
    await Task.WhenAll(tasks);

更新: 如果我想批处理任务,那会是这样吗?

foreach (var batch in items.BatchesOf(100))
{
    var tasks = batch.Select(async item =>
    {
        await DoSomething(item);
    }).ToList();

    if (tasks.Any())
    {
        await Task.WhenAll(tasks);
    }
}  

我使用的语言是C#。

2 个答案:

答案 0 :(得分:1)

这是一个可以用来异步和并行遍历树的方法:

public static async Task<IEnumerable<T>> TraverseAsync<T>(
    this IEnumerable<T> source,
    Func<T, Task<IEnumerable<T>>> childSelector)
{
    var results = new ConcurrentBag<T>();
    Func<T, Task> foo = null;
    foo = async next =>
    {
        results.Add(next);
        var children = await childSelector(next);
        await Task.WhenAll(children.Select(child => foo(child)));
    };
    await Task.WhenAll(source.Select(child => foo(child)));
    return results;
}

该方法需要一种方法来异步获取每个节点的子节点,这是您已经拥有的节点。它不是生成根节点的特殊情况,因此您需要使用您必须使用的方法将它们置于此方法的范围之外,并将它们作为此方法的第一个参数提供。

调用代码可能如下所示:

var allNodes = await new[]{await GetItemAsync(topItemId)}
    .TraverseAsync(item => GetSubItemsAsync(item.SubId));

该方法以异步方式并行获取每个节点的子节点,并在完成所有节点时将其标记为完成。然后,每个节点以递归方式递归计算所有子节点。

你已经提到你担心使用递归会因为它会占用的堆栈空间,但这不是问题,因为这些方法是异步的。每次在递归中移动一个深度时,方法都不会在堆栈上更深层次;相反,它只是调度递归方法调用以便在稍后的时间点运行,因此每个级别总是从堆栈上的固定点开始。


如果你正在寻找一种限制并行度的方法,因为担心会有太多的并行性,我首先会要求你试一试。如果您将此处的所有调用都指向线程池,那么线程池本身可能会根据其感觉可能性能最佳而具有并行数量的上限。它将停止创建更多线程,并在某个点之后将待处理项保留在队列中,并且线程池更有可能拥有一个有效的算法来确定适当的并行度。也就是说,如果你迫切需要人为地限制线程池之外的并行数量,那么肯定有方法。一种选择是创建自己的同步上下文,人为地将待处理操作的数量禁止到某个固定数字:

public class FixedDegreeSynchronizationContext : SynchronizationContext
{
    private SemaphoreSlim semaphore;
    public FixedDegreeSynchronizationContext(int maxDegreeOfParallelism)
    {
        semaphore = new SemaphoreSlim(maxDegreeOfParallelism,
            maxDegreeOfParallelism);
    }
    public override async void Post(SendOrPostCallback d, object state)
    {
        await semaphore.WaitAsync().ConfigureAwait(false);
        try
        {
            base.Send(d, state);
        }
        finally
        {
            semaphore.Release();
        }
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        semaphore.Wait();
        try
        {
            base.Send(d, state);
        }
        finally
        {
            semaphore.Release();
        }
    }
}

您可以创建上下文的实例,并在调用TraverseAsync之前将其设置为当前上下文,或者创建另一个接受maxDegreesOfParallelism的重载并在方法内设置上下文。

另一种变体是限制说出你的子选择器的调用次数,而不对这里发生的任何其他异步操作的数量施加任何限制。 (其他人都不应该特别昂贵,所以我不希望它在任何方面都很重要,但这肯定是值得尝试的东西。)为此,我们可以创建一个任务队列来处理具有固定并行度的项目,但不会人为地限制未传递给此队列的任何内容。队列本身很简单,作为同步上下文的直接变体:

public class FixedParallelismQueue
{
    private SemaphoreSlim semaphore;
    public FixedParallelismQueue(int maxDegreesOfParallelism)
    {
        semaphore = new SemaphoreSlim(maxDegreesOfParallelism,
            maxDegreesOfParallelism);
    }

    public async Task<T> Enqueue<T>(Func<Task<T>> taskGenerator)
    {
        await semaphore.WaitAsync();
        try
        {
            return await taskGenerator();
        }
        finally
        {
            semaphore.Release();
        }
    }
    public async Task Enqueue(Func<Task> taskGenerator)
    {
        await semaphore.WaitAsync();
        try
        {
            await taskGenerator();
        }
        finally
        {
            semaphore.Release();
        }
    }
}

这里,在调用方法时,您可以将此队列用作子选择器的一部分:

ar taskQueue = new FixedParallelismQueue(degreesOfParallelism);
var allNodes = await new[]{await GetItemAsync(topItemId)}
    .TraverseAsync(item => 
        taskQueue.Enqueue(() => GetSubItemsAsync(item.SubId)));

答案 1 :(得分:0)

为什么要混合异步和任务?似乎一个或另一个就足够了。

private void MainFunction(int id)
{
  var main = await GetItemAsync(id);
  await PopulateChildren(main);
}

private async void PopulateChildren(Item parent)
{
  var children = GetChildren(Item parent);
  foreach(var child in children) 
  { 
    parent.ChildCollection.Add(child); 
    PopulateChildren(child); 
  }
}

private IEnumerable<Item> GetChildren(Item parent)
{
  // I/O code
}

如果每个项目都没有保留其子项,那么您只想将它​​们保存在不同的集合中而不是parent.ChildCollection中,但代码应该相同。这不一定是深度优先的,但我不确定这是一个要求,因为做并行是你无法保证特定的订单。


好吧,让我们试试这个没有递归的东西......不过会用伪代码。

ConcurrentBag allItems;
ConcurrentBag itemsToProcess = new ConcurrentBag(initial);

// As long as it has an item...
while(itemsToProcess.TryPeek())
{
  var tasksCurrentlyProcessing;
  // Process all the items
  while(itemsToProcess.TryTake())
  {
    tasksCurrentlyProcessing = item.GetChildren();
  }
  Task.WaitAll(tasksCurrentlyProcessing);
}

public void Task GetChildren()
{
  // get children, add to allItems and itemsToProcess
}

这特别是BFS而不是DFS。不确定这是否适合你。只有这样我才能想到在没有递归的情况下做到这一点而不会让它变得非常复杂。