同时处理大型文本文件

时间:2019-06-09 23:20:51

标签: java multithreading java-8

因此,我有一个大文本文件,在这种情况下约为4.5 GB,因此我需要尽快处理整个文件。现在,我使用3个线程(不包括主线程)对此线程进行了多线程处理。输入线程用于读取输入文件,处理线程用于处理数据,输出线程用于将处理后的数据输出到文件。

当前,瓶颈是处理部分。因此,我想在混合中添加更多处理线程。但是,这造成了一种情况,我有多个线程访问同一个BlockingQueue,因此它们的结果不能保持输入文件的顺序。

我正在寻找的功能示例如下: 输入文件:1、2、3、4、5 输出文件:^相同。不是2、1、4、3、5或任何其他组合。

我编写了一个虚拟程序,该虚拟程序的功能与实际程序相同,但要减去处理部分(由于处理类包含机密信息,因此我无法提供实际程序)。我还应该提到,所有类(输入,处理和输出)都是包含在Main类中的所有内部类,Main类包含initialize()方法和下面列出的主线程代码中提到的类级别变量。

主线程:

static volatile boolean readerFinished = false; // class level variables
static volatile boolean writerFinished = false;

private void initialise() throws IOException {
    BlockingQueue<String> inputQueue = new LinkedBlockingQueue<>(1_000_000);
    BlockingQueue<String> outputQueue = new LinkedBlockingQueue<>(1_000_000); // capacity 1 million. 

    String inputFileName = "test.txt";
    String outputFileName = "outputTest.txt";

    BufferedReader reader = new BufferedReader(new FileReader(inputFileName));
    BufferedWriter writer = new BufferedWriter(new FileWriter(outputFileName));


    Thread T1 = new Thread(new Input(reader, inputQueue));
    Thread T2 = new Thread(new Processing(inputQueue, outputQueue));
    Thread T3 = new Thread(new Output(writer, outputQueue));

    T1.start();
    T2.start();
    T3.start();

    while (!writerFinished) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    reader.close();
    writer.close();

    System.out.println("Exited.");
}

输入线程:(请原谅注释的调试代码,使用它来确保读取器线程实际上正确执行了。)

class Input implements Runnable {
    BufferedReader reader;
    BlockingQueue<String> inputQueue;

    Input(BufferedReader reader, BlockingQueue<String> inputQueue) {
        this.reader = reader;
        this.inputQueue = inputQueue;
    }

    @Override
    public void run() {
        String poisonPill = "ChH92PU2KYkZUBR";
        String line;
        //int linesRead = 0;

        try {
            while ((line = reader.readLine()) != null) {
                inputQueue.put(line);
                //linesRead++;

                /*
                if (linesRead == 500_000) {
                    //batchesRead += 1;
                    //System.out.println("Batch read");
                    linesRead = 0;
                }
                */
            }

            inputQueue.put(poisonPill);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }

        readerFinished = true;

    }
}

处理线程:(通常这实际上会对行做一些事情,但是出于模型的目的,我只是将其立即推送到输出线程)。如有必要,我们可以通过使线程在每行中休眠一小段时间来模拟它的工作量。

class Processing implements Runnable {
    BlockingQueue<String> inputQueue;
    BlockingQueue<String> outputQueue;

    Processing(BlockingQueue<String> inputQueue, BlockingQueue<String> outputQueue) {
        this.inputQueue = inputQueue;
        this.outputQueue = outputQueue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                if (inputQueue.isEmpty() && readerFinished) {
                    break;
                }

                String line = inputQueue.take();
                outputQueue.put(line);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

输出线程:

class Output implements Runnable {
    BufferedWriter writer;
    BlockingQueue<String> outputQueue;

    Output(BufferedWriter writer, BlockingQueue<String> outputQueue) {
        this.writer = writer;
        this.outputQueue = outputQueue;
    }

    @Override
    public void run() {
        String line;
        ArrayList<String> outputList = new ArrayList<>();

        while (true) {
            try {
                line = outputQueue.take();

                if (line.equals("ChH92PU2KYkZUBR")) {
                    for (String outputLine : outputList) {
                        writer.write(outputLine);
                    }
                    System.out.println("Writer finished - executing termination");

                    writerFinished = true;
                    break;
                }

                line += "\n";
                outputList.add(line);

                if (outputList.size() == 500_000) {
                    for (String outputLine : outputList) {
                        writer.write(outputLine);
                    }
                    System.out.println("Writer wrote batch");
                    outputList = new ArrayList<>();
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

现在,一般数据流非常线性,看起来像这样:

输入>处理>输出。

但是我想拥有的是这样的东西:

Data flow diagram

但是要注意的是,当数据输出时,需要将其排序为正确的顺序,或者必须已经按照正确的顺序进行排序。

有关此操作的建议或示例将不胜感激。

过去,我曾使用Future和Callable接口来解决诸如此类的并行数据流的任务,但不幸的是,代码不是从单个队列中读取的,因此这里的帮助很小。

对于那些会注意到这一点的人,我还应该补充一点,batchSize和poisonPill通常是在主线程中定义的,然后通过变量传递,它们通常不是 硬编码的在输入线程的代码中,输出检查编写器线程。在凌晨1点写实验模型时,我只是有点懒。

编辑:我还应该提到,这最多是使用Java 8所必需的。由于要在运行该程序的环境中未安装这些版本,因此无法使用Java 9及更高版本的功能。

3 个答案:

答案 0 :(得分:2)

您可以做什么:

  • 使用X个线程进行处理,其中X是可用于处理的内核数
  • 为每个线程分配自己的输入队列。
  • 读取器线程以可预测的方式向每个线程的输入队列循环记录。
  • 由于输出文件太大,无法存储,因此您将编写X个输出文件,每个线程一个,并且每个文件名中都有线程的索引,以便您可以从文件名中重构原始顺序。
  • 该过程完成后,您将合并X个输出文件。再次以循环方式从线程1的文件中移出一行,从线程2的文件中移出一行,依此类推。这样就重新构成了原始顺序。

另外,由于每个线程都有一个输入队列,因此读者之间的队列没有锁争用。 (仅在读取器和写入器之间)您甚至可以通过将内容放入大于1的批处理队列中来优化此操作。

答案 1 :(得分:1)

正如Alexei所建议的那样,您可以创建OrderedTask:

class OrderedTask implements Comparable<OrderedTask> {

    private final Integer index;
    private final String line;

    public OrderedTask(Integer index, String line) {
        this.index = index;
        this.line = line;
    }


    @Override
    public int compareTo(OrderedTask o) {
        return index < o.getIndex() ? -1 : index == o.getIndex() ? 0 : 1;
    }

    public Integer getIndex() {
        return index;
    }

    public String getLine() {
        return line;
    }    
}

作为输出队列,您可以使用自己的优先级队列支持:

class OrderedTaskQueue {

    private final ReentrantLock lock;
    private final Condition waitForOrderedItem;
    private final int maxQueuesize;
    private final PriorityQueue<OrderedTask> backedQueue;

    private int expectedIndex;

    public OrderedTaskQueue(int maxQueueSize, int startIndex) {
        this.maxQueuesize = maxQueueSize;
        this.expectedIndex = startIndex;
        this.backedQueue = new PriorityQueue<>(2 * this.maxQueuesize);

        this.lock = new ReentrantLock();
        this.waitForOrderedItem = this.lock.newCondition();
    }


    public boolean put(OrderedTask item) {
        ReentrantLock lock = this.lock;
        lock.lock();
        try {
            while (this.backedQueue.size() >= maxQueuesize && item.getIndex() != expectedIndex) {
                this.waitForOrderedItem.await();
            }

            boolean result = this.backedQueue.add(item);
            this.waitForOrderedItem.signalAll();
            return result;
        } catch (InterruptedException e) {
            throw new RuntimeException();
        } finally {
            lock.unlock();
        }
    }


    public OrderedTask take() {
        ReentrantLock lock = this.lock;
        lock.lock();
        try {
            while (this.backedQueue.peek() == null || this.backedQueue.peek().getIndex() != expectedIndex) {
                this.waitForOrderedItem.await();
            }
            OrderedTask result = this.backedQueue.poll();
            expectedIndex++;
            this.waitForOrderedItem.signalAll();
            return result;
        } catch (InterruptedException e) {
            throw new RuntimeException();
        } finally {
            lock.unlock();
        }
    }
}

StartIndex 是第一个已排序任务的索引,并且 maxQueueSize 用于在我们等待一些较早的任务完成时停止处理其他任务(而不是填充内存)。它应为处理线程数的两倍/三倍,以免立即停止处理并具有可伸缩性。

然后您应该创建任务:

int indexOrder =0;
            while ((line = reader.readLine()) != null) {
                inputQueue.put(new OrderedTask(indexOrder++,line);                    

            }

仅由于您的示例而使用逐行。您应该更改OrderedTask以支持这批生产线。

答案 2 :(得分:0)

为什么不逆流?

  1. X个批次的输出调用;
  2. 生成X个承诺/任务(承诺模式),他们将随机调用处理核心之一(保留批号,以传递到输入核心);将呼叫处理程序批处理到一个有序列表中;
  3. 每个处理核心都需要在输入核心中进行批处理;
  4. 享受吗?