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

C# 如何使用异步/并行处理迭代执行深度优先搜索?,c#,.net,algorithm,task-parallel-library,async-await,C#,.net,Algorithm,Task Parallel Library,Async Await,下面是一个方法,它执行DFS搜索并返回给定顶级项id的所有项的列表。如何修改此方法以利用并行处理?当前,为堆栈中的每个项逐个调用获取子项。如果我能同时获得堆栈中多个项目的子项目,并更快地填充我的返回列表,那就太好了。我如何以线程安全的方式做到这一点(使用async/await或TPL,或其他任何方式) private async Task<IList<Item>> GetItemsAsync(string topItemId) { var items = new

下面是一个方法,它执行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;   
}
更新: 如果我想成批完成任务,你会喜欢这样的工作吗

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#。

为什么要混合异步和任务?看来这两个就足够了

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
}

这特别是BFS而不是DFS。不确定这对你是否有效。我能想到的唯一方法是不使用递归,也不使其变得非常复杂。

这里有一种方法,可以用于异步并行遍历树:

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;
}
该方法以异步方式并行获取每个节点的子节点,并在它们全部完成时将自身标记为完成。然后,每个节点递归地并行计算其所有子节点

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


如果您正在寻找一种限制并行性的方法,因为担心并行性太多,我首先请您尝试一下。如果您将这里的所有调用定向到线程池,那么线程池本身可能会根据其认为可能表现最好的情况,对并行量有一个上限。它将停止创建更多线程,并在某个时间点之后将挂起的项目保留在队列中,线程池很可能比您更具有确定适当并行度的有效算法。也就是说,如果您迫切需要人为地限制线程池之外的并行量,那么肯定有办法。一个选项是创建您自己的同步上下文,该上下文人为地将挂起操作的数量限制为某个固定数量:

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();
        }
    }
}

显示
GetSubItemsAsync
的代码。它在干什么?是IO还是CPU绑定的工作?是IO,一个REST API调用。是否可以从
堆栈中一次弹出所有项?当你处理它们时,它会生成更多的项目吗?@YuvalItzchakov我只需要指定根id下所有项目的最终列表。此方法只返回最终列表,不会进行任何其他处理,因此我认为可以从堆栈中弹出多个项目。@Prabhu我看到的主要缺点是,您似乎在强制完成整个级别的树,然后才能在其下的级别上完成任何工作。这基本上意味着花时间在树的任何深度填充然后清空工作管道,而不仅仅是在整个应用程序的开始和结束时。从本质上讲,这将是更多的代码工作,我看不出这种方法有任何令人信服的好处。谢谢。然而,这是一个递归解决方案。我特别寻找一个非递归的解决方案。有什么特别的原因吗?还有其他要求吗?你的树有多深?还不知道,而且还在生长。你的第一段我不太清楚。你介意草草记下一个电话号码样本吗?此外,我可能有数千个子项,所以我应该做这个任务。什么时候调用块?还有,很奇怪,这样一个并行解决方案就不可能使用迭代方法,或者这只是因为某种原因而不是首选方法?@Prabhu我确信迭代方法是可能的。我尝试了一段时间,但我提出的所有迭代解决方案都没有得到很好的结果;那些看似有希望的项目虽然不完整,但已经比这复杂得多了。我已经在帖子中编辑了该方法的一个示例调用。谢谢。因此,使用这个调用代码,所有节点都应该为包括自身在内的所有内容提供一个topItemId,对吗?@Prabhu由于问题的固有性质,您不能这样做。您需要先接收给定对象的响应,然后才能对所有这些子对象发出请求。您的程序已经发出了尽可能多的请求;瓶颈显然是(正如预期的)网络交互。
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();
        }
    }
}
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)));