并发目录遍历算法挂起问题

时间:2009-06-28 22:32:22

标签: java concurrency multicore fork-join

我创建了一个并发的递归目录遍历和文件处理程序,它有时会在所有并行计算完成后挂起,但“主”线程永远不会继续执行其他任务。

代码基本上是一个fork-join样式的并发聚合器,并行聚合完成后,它应该在Swing窗口中显示结果。聚合的问题在于它需要生成一个树并聚合层次结构中叶子节点的统计信息。

我确定我犯了一个并发错误但找不到它。我在帖子的末尾包含了我的代码的相关部分(为了简洁起见删除了代码注释,对于150行,如果需要,我可以将其移动到外部位置)。

上下文:Java 6u13,Windows XP SP3,Core 2 Duo CPU。

我的问题是:

这种随机挂起可能是什么原因?

是否有更好的方法进行并发目录遍历,可能采用现有库的形式?

Doug Lea(或Java 7)的fork-join框架是聚合/目录遍历的更好框架,如果是这样,我应该如何重新思考我的实现 - 在概念层面?

感谢您的时间。

代码摘录:

private static JavaFileEvaluator[] processFiles(File[] files) 
throws InterruptedException {
    CountUpDown count = new CountUpDown();
    ThreadPoolExecutor ex = (ThreadPoolExecutor)Executors
    .newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    JavaFileEvaluator[] jfes = new JavaFileEvaluator[files.length];
    for (int i = 0; i < jfes.length; i++) {
        count.increment();
        jfes[i] = new JavaFileEvaluator(files[i], count, ex);
        ex.execute(jfes[i]);
    }
    count.await();
    for (int i = 0; i < jfes.length; i++) {
        count.increment();
        final JavaFileEvaluator jfe = jfes[i];
        ex.execute(new Runnable() {
            public void run() {
                jfe.aggregate();
            }
        });

    }
    // -------------------------------------
    // this await sometimes fails to wake up
    count.await(); // <---------------------
    // -------------------------------------
    ex.shutdown();
    ex.awaitTermination(0, TimeUnit.MILLISECONDS);
    return jfes;
}
public class JavaFileEvaluator implements Runnable {
    private final File srcFile;
    private final Counters counters = new Counters();
    private final CountUpDown count;
    private final ExecutorService service;
    private List<JavaFileEvaluator> children;
    public JavaFileEvaluator(File srcFile, 
            CountUpDown count, ExecutorService service) {
        this.srcFile = srcFile;
        this.count = count;
        this.service = service;
    }
    public void run() {
        try {
            if (srcFile.isFile()) {
                JavaSourceFactory jsf = new JavaSourceFactory();
                JavaParser jp = new JavaParser(jsf);
                try {
                    counters.add(Constants.FILE_SIZE, srcFile.length());
                    countLines();
                    jp.parse(srcFile);
                    Iterator<?> it = jsf.getJavaSources();
                    while (it.hasNext()) {
                        JavaSource js = (JavaSource)it.next();
                        js.toString();
                        processSource(js);
                    }
                // Some catch clauses here
                }
            } else
            if (srcFile.isDirectory()) {
                processDirectory(srcFile);
            }
        } finally {
            count.decrement();
        }
    }
    public void processSource(JavaSource js) {
        // process source, left out for brevity
    }
    public void processDirectory(File dir) {
        File[] files = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File pathname) {
                return 
                (pathname.isDirectory() && !pathname.getName().startsWith("CVS") 
                 && !pathname.getName().startsWith("."))
                || (pathname.isFile() && pathname.getName().endsWith(".java") 
                 && pathname.canRead());
            }
        });
        if (files != null) {
            Arrays.sort(files, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    if (o1.isDirectory() && o2.isFile()) {
                        return -1;
                    } else
                    if (o1.isFile() && o2.isDirectory()) {
                        return 1;
                    }
                    return o1.getName().compareTo(o2.getName());
                }
            });
            for (File f : files) {
                if (f.isFile()) {
                    counters.add(Constants.FILE, 1);
                } else {
                    counters.add(Constants.DIR, 1);
                }
                JavaFileEvaluator ev = new JavaFileEvaluator(f, count, service);
                if (children == null) {
                    children = new ArrayList<JavaFileEvaluator>();
                }
                children.add(ev);
                count.increment();
                service.execute(ev);
            }
        }
    }
    public Counters getCounters() {
        return counters;
    }
    public boolean hasChildren() {
        return children != null && children.size() > 0;
    }
    public void aggregate() {
        // recursively aggregate non-leaf nodes
        if (!hasChildren()) {
            count.decrement();
            return;
        }
        for (final JavaFileEvaluator e : children) {
            count.increment();
            service.execute(new Runnable() {
                @Override
                public void run() {
                    e.aggregate();
                }
            });
        }
        count.decrement();
    }
}
public class CountUpDown {
    private final Lock lock = new ReentrantLock();
    private final Condition cond = lock.newCondition();
    private final AtomicInteger count = new AtomicInteger();
    public void increment() {
        count.incrementAndGet();
    }
    public void decrement() {
        int value = count.decrementAndGet();
        if (value == 0) {
            lock.lock();
            try {
                cond.signalAll();
            } finally {
                lock.unlock();
            }
        } else
        if (value < 0) {
            throw new IllegalStateException("Counter < 0 :" + value);
        }
    }
    public void await() throws InterruptedException {
        lock.lock();
        try {
            if (count.get() > 0) {
                cond.await();
            }
        } finally {
            lock.unlock();
        }
    }
}

编辑在JavaSourceEvaluator中添加了hasChildren()方法。

1 个答案:

答案 0 :(得分:1)

在JavaFileEvaluator的聚合方法中,不在finally块中调用count.decrement()。如果在聚合函数内部抛出任何RuntimeExceptions(可能在hasChildren方法中,我看不到它的主体?),则永远不会发生对递减的调用,并且CountUpDown将无限期地保持等待状态。这可能是您看到随机挂起的原因。

对于第二个问题,我不知道java中的任何库是这样做的,但是我没有真正看过,抱歉没有答案,但这不是我有机会使用的东西之前。

就第三个问题而言,我认为无论你是否使用其他人提供的fork-join框架,或者继续提供自己的并发框架,最大的收获就是分离执行遍历工作的逻辑。来自管理并行性的逻辑的目录。您提供的代码使用CountUpDown类来跟踪所有线程何时完成,并最终调用遍及遍历目录遍历的方法的增量/减量调用,这将导致恶梦跟踪错误。转移到java7 fork-join框架将强制您创建一个仅处理实际遍历逻辑的类,并将并发逻辑保留到框架中,这可能是您的好方法。另一个选择是继续使用你在这里的内容,但在管理逻辑和工作逻辑之间做一个清晰的描述,这将有助于你找到并修复这些类型的错误。