簡體   English   中英

對於Java生產者-消費者來說,更好的習慣用法是什么?

[英]What is a better idiom for producer-consumers in Java?

我想逐行讀取文件,對每行進行一些緩慢的操作(可以輕松並行完成),然后將結果逐行寫入文件。 我不在乎輸出的順序。 輸入和輸出非常大,無法容納在內存中。 我希望能夠對同時運行的線程數以及內存中的行數設置硬性限制。

我用於文件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,當我真的只想等待所有工作人員完成時被迫指定超時等似乎很棘手,總之一個100多行的方法,或者什至幾百行代碼(如果我將Runnables設為自己的類),這似乎是一個非常基本的模式。

有更好的解決方案嗎? 我想盡可能地利用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);
        }
    });
}

首先,我想指出三種可能的情況:

1.-對於文件的所有行,使用doStuff方法處理該行所需的時間大於從磁盤讀取同一行並進行解析所需的時間

2.-對於文件的所有行,使用doStuff方法處理一行所需的時間小於或等於讀取同一行並對其進行解析所花費的時間。

3.-同一文件的第一種情況和第二種情況都沒有。

您的解決方案應該適合第一種情況,但不適用於第二種或第三種情況,而且,您不是以同步方式修改隊列。 甚至,如果您遇到的是數字2之類的場景,那么當沒有數據要發送到輸出時,或者沒有行要發送到隊列中要由doStuff處理的行時,您就浪費了CPU周期。 ,通過旋轉:

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

最后,無論您遇到哪種情況,我都建議您使用Monitor對象,這將使您可以放置​​特定線程,直到另一個進程通知它們某個條件為真並可以再次激活它們為止。 通過使用Monitor對象,您不會浪費cpu周期。

有關更多信息,請參見: https : //docs.oracle.com/javase/7/docs/api/javax/management/monitor/Monitor.html

編輯:我刪除了使用同步方法的建議,因為正如您所指出的那樣,BlockingQueue的方法是線程安全的(或幾乎所有的),並防止出現競爭情況。

使用ThreadPoolExecutor綁定到固定大小的阻塞隊列,您的所有復雜性都將在JavaDoc中消失。

只需一個線程讀取文件並阻塞阻塞隊列,所有處理便由執行程序完成。

附加物:

您可以在編寫器上進行同步,也可以僅使用另一個隊列,然后處理器將其填滿,而單個寫入線程將占用該隊列。

與編寫者同步最有可能是最簡單的方法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM