Java中生产者 - 消费者的习惯是什么?

时间:2016-03-20 00:27:25

标签: java multithreading concurrency parallel-processing blocking

我想逐行读取文件,对每条可以轻松并行完成的行做一些缓慢的操作,并逐行将结果写入文件。我不关心输出的顺序。输入和输出都很大,不适合记忆。我希望能够对同时运行的线程数以及内存中的行数设置硬限制。

我用于文件IO(Apache Commons CSV)的库似乎不提供同步文件访问,所以我不认为我可以从同一个文件读取或一次从多个线程写入同一个文件。如果可能的话,我会创建一个ThreadPoolExecutor并为每行提供一个任务,这将只是读取行,执行计算并写入结果。

相反,我认为我需要的是一个执行解析的线程,一个用于解析输入行的有界队列,一个包含进行计算的作业的线程池,一个用于计算输出行的有界队列,以及一个写作的线程。生产者,许多消费者 - 生产者和消费者,如果这是有道理的。

我看起来像这样:

BlockingQueue<CSVRecord> inputQueue = new ArrayBlockingQueue<CSVRecord>(INPUT_QUEUE_SIZE);
BlockingQueue<String[]> outputQueue = new ArrayBlockingQueue<String[]>(OUTPUT_QUEUE_SIZE);

Thread parserThread = new Thread(() -> {
    while (inputFileIterator.hasNext()) {
        CSVRecord record = inputFileIterator.next();
        parsedQueue.put(record); // blocks if queue is full
    }
});

// the job queue of the thread pool has to be bounded too, otherwise all 
// the objects in the input queue will be given to jobs immediately and 
// I'll run out of heap space
// source: https://stackoverflow.com/questions/2001086/how-to-make-threadpoolexecutors-submit-method-block-if-it-is-saturated
BlockingQueue<Runnable> jobQueue = new ArrayBlockingQueue<Runnable>(JOB_QUEUE_SIZE);
RejectedExecutionHandler rejectedExecutionHandler 
    = new ThreadPoolExecutor.CallerRunsPolicy();
ExecutorService executorService 
    = new ThreadPoolExecutor(
        NUMBER_OF_THREADS, 
        NUMBER_OF_THREADS, 
        0L, 
        TimeUnit.MILLISECONDS, 
        jobQueue, 
        rejectedExecutionHandler
    );
Thread processingBossThread = new Thread(() -> {
    while (!inputQueue.isEmpty() || parserThread.isAlive()) {
        CSVRecord record = inputQueue.take(); // blocks if queue is empty
        executorService.execute(() -> {
            String[] array = this.doStuff(record);
            outputQueue.put(array); // blocks if queue is full
        });
    }
    // getting here that means that all CSV rows have been read and 
    // added to the processing queue
    executorService.shutdown(); // do not accept any new tasks
    executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); 
        // wait for existing tasks to finish
});

Thread writerThread = new Thread(() -> {
    while (!outputQueue.isEmpty() || consumerBossThread.isAlive()) {
        String[] outputRow = outputQueue.take(); // blocks if queue is empty
        outputFileWriter.printRecord((Object[]) outputRow);
});

parserThread.start();
consumerBossThread.start();
writerThread.start();

// wait until writer thread has finished
writerThread.join();

我遗漏了日志记录和异常处理,所以这看起来比它短得多。

此解决方案有效,但我对它不满意。似乎很难创建我自己的线程,检查他们的isAlive(),在Runnable中创建一个Runnable,当我真的只想等到所有工人完成等时被强制指定超时等等。总而言之它&如果我让Runnables成为他们自己的类,那就是100多行方法,甚至几百行代码,看似非常基本的模式。

有更好的解决方案吗?我希望尽可能多地使用Java库,以帮助保持我的代码可维护性并符合最佳实践。我仍然想知道它在幕后做了什么,但我怀疑自己实现这一切是最好的方法。

更新 根据答案的建议,更好的解决方案:

BlockingQueue<Runnable> jobQueue = new ArrayBlockingQueue<Runnable>(JOB_QUEUE_SIZE);
RejectedExecutionHandler rejectedExecutionHandler
    = new ThreadPoolExecutor.CallerRunsPolicy();
ExecutorService executorService 
    = new ThreadPoolExecutor(
        NUMBER_OF_THREADS, 
        NUMBER_OF_THREADS, 
        0L, 
        TimeUnit.MILLISECONDS, 
        jobQueue, 
        rejectedExecutionHandler
    );

while (it.hasNext()) {
    CSVRecord record = it.next();
    executorService.execute(() -> {
        String[] array = this.doStuff(record);
        synchronized (writer) {
            writer.printRecord((Object[]) array);
        }
    });
}

2 个答案:

答案 0 :(得分:1)

我想首先指出一些事情,我可以想到三种可能的情况:

1.-对于文件的所有行,使用doStuff方法处理行所需的时间大于从磁盘读取同一行并解析它所需的时间

2.-对于文件的所有行,使用doStuff方法处理行所需的时间小于或等于读取同一行并解析它所需的时间。 / p>

3.-同一文件的第一个和第二个场景都没有。

您的解决方案应该适用于第一种方案,但不适用于第二种或第三种方案,此外,您不会以同步方式修改队列。更重要的是,如果您遇到类似2的情况,那么当没有数据要发送到输出时,或者没有要发送到队列的行时,您就会浪费cpu周期由doStuff处理,通过以下方式进行处理:

while (!outputQueue.isEmpty() || consumerBossThread.isAlive()) {

最后,无论您遇到哪种情况,我建议您使用Monitor对象,这将允许您将特定线程等待,直到另一个进程通知他们某个条件为真,并且他们可以再次激活。通过使用Monitor对象,您不会浪费cpu周期。

有关详细信息,请参阅: https://docs.oracle.com/javase/7/docs/api/javax/management/monitor/Monitor.html

编辑:我已经删除了使用同步方法的建议,因为正如您所指出的,BlockingQueue的方法是线程安全的(或几乎所有方法)并且可以防止竞争条件。

答案 1 :(得分:1)

使用绑定到固定大小阻塞队列的ThreadPoolExecutor,您的所有复杂性都会在JavaDoc中消失。

只需要一个线程读取文件并插入阻塞队列,所有处理都由执行者完成。

附录:

您既可以在编写器上进行同步,也可以只使用另一个队列,并且处理器填充该队列,并且您的单个写入线程会占用该队列。

在编写器上进行同步很可能是最简单的方法。