Многопоточность и рекурсия вместе

У меня есть рекурсивный код, который сначала обрабатывает древовидную структуру. Код в основном выглядит так:

function(TreeNode curr) 
{
    if (curr.children != null && !curr.children.isEmpty()) 
    {
        for (TreeNode n : curr.children) 
    {
            //do some stuff
            function(n);
        }
    } 
    else 
    {
        //do some other processing
    }
}

Я хочу использовать потоки, чтобы сделать это быстрее. Большая часть времени тратится на перемещение, поэтому я не хочу просто создавать поток для обработки "другой обработки", потому что это не займет много времени. Я думаю, что хочу развить темы На "сделать некоторые вещи", но как это будет работать?

3 ответов


Это хороший случай для Fork / Join framework который должен быть включен в Java 7. В качестве отдельной библиотеки для использования с Java 6 можно скачать здесь.

что-то вроде этого:

public class TreeTask extends RecursiveAction {
    private final TreeNode node;
    private final int level;

    public TreeTask(TreeNode node, int level) {
        this.node = node;
        this.level = leve;
    }

    public void compute() {
        // It makes sense to switch to single-threaded execution after some threshold
        if (level > THRESHOLD) function(node);

        if (node.children != null && !node.children.isEmpty()) {
            List<TreeTask> subtasks = new ArrayList<TreeTask>(node.children.size());
            for (TreeNode n : node.children) {
                // do some stuff
                subtasks.add(new TreeTask(n, level + 1));
            }
            invokeAll(subtasks); // Invoke and wait for completion
        } else {
            //do some other processing
        }
    }
}

...
ForkJoinPool p = new ForkJoinPool(N_THREADS);
p.invoke(root, 0);

ключевым моментом Fork / join framework является кража работы-в ожидании завершения подзадач поток выполняет другие задачи. Это позволяет писать алгоритм простым способом, избегая при этом проблем с исчерпанием потока как наивный подходы с ExecutorService бы.


на // do some stuff блок кода, где вы работаете на отдельном узле, вместо этого вы можете отправить узел в какой-то ExecutorService (в виде Runnable которая будет работать на узле).

вы можете настроить ExecutorService что вы используете для поддержки пула определенного количества потоков, что позволяет отделить логику" обработки " (наряду с логикой создания потоков, сколько создавать и т. д.) От вашей логики анализа дерева.


Это решение предполагает, что обработка происходит только в листовых узлах и что фактическая рекурсия дерева не занимает много времени.

Я бы попросил вызывающий поток выполнить рекурсию, а затем BlockingQueue рабочих, которые обрабатывают листья через пул потоков. Я не справляюсь с InterruptedException в нескольких местах здесь.

public void processTree(TreeNode top) {
    final LinkedBlockingQueue<Runnable> queue =
        new LinkedBlockingQueue<Runnable>(MAX_NUM_QUEUED);
    // create a pool that starts at 1 threads and grows to MAX_NUM_THREADS
    ExecutorService pool =
        new ThreadPoolExecutor(1, MAX_NUM_THREADS, 0L, TimeUnit.MILLISECONDS, queue,
            new RejectedExecutionHandler() {
                public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                    queue.put(r);  // block if we run out of space in the pool
                }
            });
    walkTree(top, pool);
    pool.shutdown();
    // i think this will join with all of the threads
    pool.awaitTermination(WAIT_TILL_CHILDREN_FINISH_MILLIS, TimeUnit.MILLISECONDS);
}
private void walkTree(final TreeNode curr, ExecutorService pool) {
    if (curr.children == null || curr.children.isEmpty()) {
        pool.submit(new Runnable() {
            public void run() {
                processLeaf(curr);
            }
        });
        return;
    }
    for (TreeNode child : curr.children) {
        walkTree(child, pool);
    }
}
private void processLeaf(TreeNode leaf) {
    // ...
}