1

As the title says, I'm looking for the best way to do a parallel BFS in C#. I want it to be as fast as possible and use as little memory as possible. If there is no 'best' way, what are good options?

I want to use this for solving a puzzle (in my case Rush Hour). My current algorithms use a taboo HashSet which contains all previously explored states. This prevents high memory usage and significantly increases the speed of the algorithm. I currectly have a sequential and a parallel version of my algorithm. The sequential version is always faster or equally fast. This is my current parallel code:

public static void SolveParallel(Board start)
{
    ConcurrentQueue<Board> q = new ConcurrentQueue<Board>();
    q.Enqueue(start);
    HashSet<Board> taboo = new HashSet<Board>();
    taboo.Add(start);
    Board goal = null;

    ParallelOptions po = new ParallelOptions();
    po.MaxDegreeOfParallelism = Environment.ProcessorCount;

    while ((q.Count > 0) && (goal == null))
    {
        Parallel.ForEach<Board>(q.Take(q.Count), po, (b, state) =>
        {
            foreach (Board succ in b.Successors(taboo))
            {
                if (succ.Solved)
                {
                    state.Stop();
                    goal = succ;
                    return;
                }
                else
                    q.Enqueue(succ);
            }
        });
    }
    // Print solution (ignore this).
    if (goal != null)
        PrintSolution(goal);
    else
        Console.WriteLine("No solutions found");
}

Now there are problems with this I can see myself.

  1. ConcurrentQueue is not wait-free. I assume it has built-in locks which makes it use extra time.
  2. If the solution is found, other iterations of the Parallel.ForEach will continue running. I have tried solving this with cancellation tokens and parallel states, but it doesn't seem to work (or at least it doesn't improve the running time).
  3. States can be explored multiple times, since a state can be found in multiple iterations of the foreach loop before it is added to the taboo list.
  4. I'm not sure if Parallel.ForEach is the best way to solve my problem.

I have searched around the internet, including stackoverflow, but I can't find any algorithm that solves the problem. Everyone seems to use Parallel.ForEach and ConcurrentQueue. But I'm sure there are better options.

Any help is appreciated!

Wouter Florijn
  • 2,711
  • 2
  • 23
  • 38
  • You should tag this question with 'C#' too I think. – julealgon Nov 06 '13 at 12:56
  • What's the nature of your graph? Tree? DAG? or a general graph? – CodesInChaos Nov 06 '13 at 13:16
  • `q.Take(q.Count)` this is racy - this sequence might miss enqueued items. – usr Nov 06 '13 at 13:29
  • @CodesInChaos The graph is constructed during the algorithm. The neighbours/children of a node are found with `b.Successors(taboo)`. This method returns all successor states of b that aren't in the taboo list, and adds them to the taboo list. @usr What exactly do you mean? To clarify, my algorithm always finds a correct and optimal solution. It's just slow in some cases. – Wouter Florijn Nov 06 '13 at 13:34
  • 1
    @WouterFlorijn what if the sequence runs empty just before `q.Enqueue(succ);` runs. `Parallel.ForEach` will terminate then and an items will still be left in the queue. You algorithm is correct just 99.9% of the time :) – usr Nov 06 '13 at 13:44
  • Try using `ConcurrentBag`. It prefers thread-local operations. At the moment, synchronization costs are killing you (assuming that `Successors` is reasonably cheap). Even lock/wait-free synchronization does not scale if you contend on the same memory locations (which you are, inside of the queue). `Interlocked` operations just use cheap hardware locks on single cache-lines. – usr Nov 06 '13 at 13:47
  • @usr the queue count is only checked after the `Parallel.ForEach` is completed. So all `Enqueues` have already been executed (correct me if I'm wrong but I'm pretty sure of this). I'll try `ConcurrentBag`. – Wouter Florijn Nov 06 '13 at 15:10
  • `ConcurrentBag` seems to be equally fast (slow) as `ConcurrentQueue`. – Wouter Florijn Nov 06 '13 at 15:18
  • Parallel.ForEach calls `MoveNext` on the iterator until it returns false. Once it has returned false, it never calls again. Even when tasks are still in-flight. That's just then normal way to enumerate a sequence. – usr Nov 06 '13 at 15:57

0 Answers0